├── .github
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── MEDIAPACKAGE_CONFIG.md
├── NOTICE
├── README.md
├── autopep8.sh
├── cloudformation
├── mediapackage_endpoint_common.py
├── mediapackage_speke_endpoint.json
├── mediapackage_speke_endpoint.py
├── resource_tools.py
└── speke_reference.json
├── docs
├── behavioral-views.drawio
├── bv-key-generation.jpg
├── bv-key-retrieval.jpg
├── deployment-view.drawio
├── deployment-view.jpg
├── logical-view.drawio
├── logical-view.jpg
├── physical-view.drawio
├── physical-view.jpg
├── software-architecture.md
├── use-cases.drawio
└── use-cases.jpg
├── images
├── speke-reference.png
└── speke-s3-cache.png
├── lambda
└── speke_libs
│ ├── .gitignore
│ └── requirements.txt
├── local_build.sh
├── misc
└── sync_commands.py
├── pylint.sh
├── requirements.txt
├── spekev2_verification_testsuite
├── .gitignore
├── Pipfile
├── Pipfile.lock
├── README.md
├── __init__.py
├── conftest.py
├── helpers
│ ├── __init__.py
│ ├── generate_test_artifacts.py
│ ├── speke_element_assertions.py
│ └── utils.py
├── pytest.ini
├── spekev2_requests
│ ├── general
│ │ ├── 1_generic_spekev2_dash_widevine_preset_video_1_audio_1_no_rotation.xml
│ │ ├── 2_speke_v1_style_implementation.xml
│ │ ├── 3_negative_wrong_version_spekev2_dash_widevine.xml
│ │ ├── 4_spekev2_negative_preset_shared_video.xml
│ │ └── 5_spekev2_negative_preset_shared_audio.xml
│ └── vod
│ │ ├── 1_generic_spekev2_dash_widevine_preset_video_1_audio_1_no_rotation.xml
│ │ ├── 2_speke_v1_style_implementation.xml
│ │ ├── 3_negative_wrong_version_spekev2_dash_widevine.xml
│ │ ├── 4_spekev2_negative_preset_shared_video.xml
│ │ └── 5_spekev2_negative_preset_shared_audio.xml
├── test_basic_checks.py
├── test_case_1_preset_video_1_preset_audio_1.py
├── test_case_2_preset_video_3_preset_audio_2.py
├── test_case_3_preset_video_5_preset_audio_3.py
├── test_case_4_preset_video_8_preset_audio_2.py
├── test_case_5_preset_video_2_audio_unencrypted.py
├── test_case_6_preset_video_unencrypted_a_2.py
├── test_check_mandatory_elements.py
├── test_drm_system_specific_system_id_elements.py
└── test_negative_cases.py
├── src
├── key_cache.py
├── key_generator.py
├── key_server.py
├── key_server_common.py
└── zappa_settings.json
├── tests
├── README.md
├── api_gateway_tests.py
├── lambda_tests.py
├── requirements.txt
├── server_api_body.xml
└── server_api_body_spekev2.xml
├── workflow
├── drm-live.md
├── drm-vod.md
└── images
│ ├── live_mediapackage-encryption_config.png
│ ├── live_mediapackage-endpoints.png
│ ├── live_mediapackage-preview-hls.png
│ ├── live_mediapackage_drm_config.png
│ ├── vod_custom_templates.png
│ ├── vod_drm_settings.png
│ ├── vod_hls_output_group.png
│ ├── vod_lambda_input_validate.png
│ └── vod_lambda_template.png
└── yapf.sh
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | *Issue #, if available:*
2 |
3 | *Description of changes:*
4 |
5 |
6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | ~*
3 | *~
4 | __pycache__/
5 | *.zip
6 | .vscode/
7 | build/
8 | env/
9 | .idea/
10 | Pipfile
11 | reports/
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct for SPEKE Reference Server
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [speke-github-team@amazon.com](email:speke-github-team@amazon.com). The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
48 | [**Home**](README.md)
49 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to SPEKE Reference Server
2 |
3 | When contributing to this repository, please first discuss the change you wish to make via issue,
4 | email, or any other method with the owners of this repository before making a change.
5 |
6 | Third-party patches are essential for growing and enhancing SPEKE Reference Server. We want to keep it as easy as possible to contribute changes that
7 | get things working in your environment. There are a few guidelines that we
8 | need contributors to follow so that we can have a chance of keeping on
9 | top of things.
10 |
11 | Please note we have a code of conduct, please follow it in all your interactions with the project.
12 |
13 | ## SPEKE Reference Server Core vs Extensions
14 |
15 | New functionality is typically directed toward extensions to provide a slimmer
16 | SPEKE Reference Server Core, reducing its surface area, and to allow greater freedom for
17 | module maintainers to ship releases at their own cadence.
18 |
19 | Generally, non-backward compatible and OS-specific changes should be added as optional enhancements. Exceptions would be things like new cross-OS features and updates to existing core types.
20 |
21 | If you are unsure of whether your contribution should be implemented as an optional enhancement or part of SPEKE Reference Server Core, please first discuss the change you wish to make via issue,
22 | email, or any other method with the owners of this repository.
23 |
24 | ## Pull Request Process
25 |
26 | 1. Write one or more test cases to demonstrate how your feature, extension or fix operates. Include the test cases with your pull request.
27 | 2. Ensure any install or build dependencies are removed before submitting your pull request.
28 | 2. Update the README.md with details of changes to the solution. Include new environment
29 | variables, exposed ports, useful file locations and service parameters.
30 | 3. Increase the version numbers in any examples files and the README.md to the new version that this
31 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
32 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
33 | do not have permission to do that, you may request the second reviewer to merge it for you.
34 |
35 | ## Revert Policy
36 |
37 | By running tests in advance and by engaging with peer review for prospective
38 | changes, your contributions have a high probability of becoming long lived
39 | parts of the the project.
40 |
41 | If the code change results in a failure, we will make our best effort to
42 | correct the error. If a fix cannot be determined and committed within 24 hours
43 | of its discovery, the commit(s) responsible _may_ be reverted, at the
44 | discretion of the committer and SPEKE Reference Server maintainers. This action would be taken
45 | to help maintain passing states in our testing pipelines.
46 |
47 | The original contributor will be notified of the revert in the GitHub Issue
48 | associated with the change. A reference to the test(s) and operating system(s)
49 | that failed as a result of the code change will also be added to the GitHub Issue. This test(s) should be used to check future submissions of the code to
50 | ensure the issue has been resolved.
51 |
52 | [**Home**](README.md)
53 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/MEDIAPACKAGE_CONFIG.md:
--------------------------------------------------------------------------------
1 | ## AWS Elemental MediaPackage Configuration for SPEKE Reference Server
2 |
3 | The following page guides the user through configuration of AWS Elemental MediaPackage to utilize the SPEKE Reference Server. At the end of this page you will have created an HLS encrypted endpoint for an existing AWS MediaPackage channel that uses the SPEKE server deployed from this project.
4 |
5 | ## Prerequisites
6 |
7 | - An AWS account with administrator rights and access to the AWS console.
8 | - An AWS MediaPackage channel that is currently ingesting video from an encoder, such as AWS MediaLive.
9 | - If you need to create a media workflow with AWS MediaLive and AWS MediaPackage quickly, consider deploying the LiveStreamingWorkshopResources.json template provided by this GitHub project as a first step: https://github.com/aws-samples/aws-media-services-simple-live-workflow.
10 | - Note that this solution can be deployed to any region that supports API Gateway, Lambda, and S3 and Media Services. You need to consider the packager or encoder's location relative to the API Gateway endpoint used to create encryption keys. The encoder, packager and SPEKE services should be in the same region or as geographically close as possible to reduce the request/response latency in key generation.
11 |
12 | ## Required Information
13 |
14 | - Channel ID of the AWS MediaPackage channel
15 | - Role ARN of the `MediaPackageInvokeSPEKERole` previously created by the reference template
16 | - Rotation Interval is the number seconds a new key is required during live play
17 | - Server URL is the API Gateway URL of the SPEKE server that ends with the path /EkeStage/copyProtection
18 |
19 | ## CloudFormation Templates
20 |
21 | 1. Sign in to the AWS console.
22 | 1. Choose a region such as us-east-1 or us-west-2. You should have the SPEKE server, encoder and packager running in the same AWS Region.
23 | 1. Navigate to the AWS CloudFormation console.
24 | 1. Create a new stack.
25 | 1. On the `Select Template` page, provide the local or hosted copy of the template. The CloudFormation template is is named `mediapackage_speke_endpoint.json` and is available in the cloudformation folder in this project, and also hosted here by the project sponsors: `https://s3.amazonaws.com/rodeolabz-us-east-1/speke/mediapackage_speke_endpoint.json`
26 | 1. At the `Specify Details` pages, provide a stack name, like `ENDPOINT`.
27 | 1. Provide the information you collected from the `Required Information` section above.
28 | 2. The `Options` page does not require any input, although you can choose to be notified after the template completes.
29 |
30 | When the template is complete you will have an operational reference SPEKE server that can be used for HLS encryption. You can review the Resources tab of the template to see what was created or updated.
31 |
32 | Navigate to the AWS MediaPackage channel and look for the endpoint with `MediaPackageEncryptedEndpoint` in the name.
33 |
34 | You can play the endpoint with Safari, QuickTime Player, or an HLS-compatible browser player such as Video.js.
35 |
36 |
37 | ## Manual Configuration
38 |
39 |
40 | 1. Create an HLS endpoint and select Encrypt content.
41 | 1. For Resource ID, enter any valid string. Consider using a UUID or GUID.
42 | 1. For System ID, enter
43 | ```
44 | 81376844-f976-481e-a84e-cc25d39b0b33
45 | ```
46 | 1. The system ID is part of the DASH-IF CPIX standard, which we have adopted for our key exchange protocol. It defines the DRM system. System ID is defined for DASH Widevine, DASH PlayReady, and so on. When setting up your own HLS DRM solution, you can configure whatever you want on the endpoint, as long as your key server knows what to do with it.
47 | 1. For URL, paste in the API Gateway URL of the SPEKE server that ends with the path /EkeStage/copyProtection, as in:
48 | ```
49 | https://ayh8dwke5b.execute-api.us-west-2.amazonaws.com/EkeStage/copyProtection
50 | ```
51 | 1. For Role ARN, paste in the `MediaPackageInvokeSPEKERole` previously created by the reference template
52 | 1. Under Additional configuration, leave the Encryption Method at AES-128
53 | 2. Set the key rotation interval to 60 seconds
54 | 1. Save the endpoint
55 |
56 | The output should now be encrypted (you can check by downloading the manifests). The endpoint should play back on Safari and Quicktime on a Mac.
57 |
58 |
59 | ## HLS Manifest Check
60 |
61 | The second-level HLS manifests will include key retrieval information in the EXT-X-KEY label for one or more segments.
62 | ```
63 | #EXTM3U
64 | #EXT-X-VERSION:4
65 | #EXT-X-TARGETDURATION:7
66 | #EXT-X-MEDIA-SEQUENCE:9922
67 | #EXT-X-KEY:METHOD=AES-128,URI="https://d33vycj6dbbmzj.cloudfront.net/7e4de17c-a065-4ce8-874f-f3ed68c5c5f1/f50e4740-d16e-4374-a2e5-4f43134b3cf2"
68 | #EXTINF:6.006,
69 | index_9_9922.ts?m=1524544797
70 | #EXTINF:6.006,
71 | index_9_9923.ts?m=1524544797
72 | #EXTINF:6.006,
73 | index_9_9924.ts?m=1524544797
74 | #EXTINF:6.006,
75 | index_9_9925.ts?m=1524544797
76 | #EXTINF:6.006,
77 | index_9_9926.ts?m=1524544797
78 | #EXTINF:6.006,
79 | index_9_9927.ts?m=1524544797
80 | #EXTINF:6.006,
81 | index_9_9928.ts?m=1524544797
82 | #EXTINF:6.006,
83 | index_9_9929.ts?m=1524544797
84 | #EXT-X-KEY:METHOD=AES-128,URI="https://d33vycj6dbbmzj.cloudfront.net/7e4de17c-a065-4ce8-874f-f3ed68c5c5f1/62e65271-1e30-4e0d-aed5-06a947028d99"
85 | #EXTINF:6.006,
86 | index_9_9930.ts?m=1524544797
87 |
88 |
89 | ```
90 |
91 | [**Home**](README.md)
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | SPEKE Reference Server
2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## SPEKE Reference Server
2 |
3 | Secure Packager and Encoder Key Exchange (SPEKE) is part of the AWS Elemental content encryption protection strategy for media services customers. SPEKE defines the standard for communication between AWS Media Services and digital rights management (DRM) system key servers. SPEKE is used to supply keys to encrypt video on demand (VOD) content through AWS Elemental MediaConvert and for live content through AWS Elemental MediaPackage.
4 |
5 | Take a look at [high-level SPEKE documentation](https://docs.aws.amazon.com/speke/latest/documentation/what-is-speke.html) available on the AWS web site.
6 |
7 |
8 | ## Setup
9 |
10 | Use the provided CloudFormation template to deploy the reference key server into your AWS account. The reference SPEKE implementation provides a key server and key distribution cache for end-to-end segment encyption with HLS and DASH. Use it as an example and starting point when implementing a complete DRM solution with SPEKE.
11 |
12 | The CloudFormation template creates an API Gateway, Lambda function, S3 bucket and CloudFront distribution and adds the needed settings for the reference server. Additionally, the template creates IAM policies and roles necessary for API Gateway, Lambda, Secrets Manager, S3 and CloudFront to interact.
13 |
14 | The following diagram shows the primary components of the serverless SPEKE solution and the connectivity among the components during runtime. The diagram also shows one possible integration between AWS MediaPackage or AWS MediaConvert and SPEKE.
15 |
16 | 
17 |
18 |
19 | These sections will guide you through installation, testing and configuration of the SPEKE Reference Server.
20 |
21 | 1. [**Installation**](#speke-reference-server-installation) - This section includes installation instructions for API Gateway, Lambda deployment and AWS Elemental MediaPackage channel integration.
22 |
23 | 2. [**Test Cases**](tests/README.md) - This page include several unit tests and manual test cases that can be used to verify operation of the SPEKE Reference Server. These test cases do not require integration with additional services.
24 |
25 | 3. [**AWS Elemental MediaPackage**](MEDIAPACKAGE_CONFIG.md) - This page documents steps that can be used to verify operation of the SPEKE Reference Server using AWS Elemental MediaPackage.
26 |
27 | 4. [**Contributing**](CONTRIBUTING.md) - This page includes the guidelines for contributing your enhancements, fixes and documentation to the project.
28 |
29 | 5. [**Code of Conduct**](CODE_OF_CONDUCT.md) - This is what we expect from all people interacting and contributing with the team.
30 |
31 |
32 |
33 | ## SPEKE Reference Server Installation
34 |
35 | The following page guides the user through deployment and configuration of the SPEKE Reference Server.
36 |
37 | ### Prerequisites
38 |
39 | - An AWS account with administrator rights and access to the AWS console
40 | - Note that this solution can be deployed to any region that supports API Gateway, Lambda, and S3. You need to consider the packager or encoder's location relative to the API Gateway endpoint used to create encryption keys. The encoder, packager and SPEKE services should be in the same region or as geographically close as possible to reduce the request/response latency in key generation.
41 |
42 | ### Building Cloudformation template and Lambda locally
43 |
44 | 1. Create a virtual environment for this project using python3 using steps outlined [here](https://docs.python.org/3/tutorial/venv.html).
45 | 1. Install dependencies within the virtual environment using `pip3 install -r requirements.txt`.
46 | 1. In `zappa_settings.json` under `src`, replace `aws_region` with the region this lambda will be deployed.
47 | 1. Run `local_build.sh`. If you are working on Mac/Windows, run the script with `REQUIRES_SPEKE_SERVER_LAMBDA_LAYER=true` to generate `speke-libs` lambda layer zip file. Note that Docker is required to build the zip file. See the [sidenote](#sidenote-building-the-lambda-on-macwindows) below for more details about the lambda layer.
48 | 1. The script will generate required artifacts under `build` folder.
49 | 1. Create a new bucket in S3 (For example: `speke-us-east-1`). Create a folder called `speke` and upload the generated `speke-reference` lambda zip file. If you build with `REQUIRES_SPEKE_SERVER_LAMBDA_LAYER=true`, upload the generated `speke-libs` lambda layer zip file to the same folder too.
50 | 1. In the generated `speke_reference.json`, replace `rodeolabz` with the name of your created bucket (`speke` is used in this example).
51 | 1. Use the `speke_reference.json` template in CloudFormation to deploy the speke reference server following the instructions below.
52 |
53 | #### **Sidenote:** Building the lambda on Mac/Windows
54 | AWS Lambda environment is similar to Amazon Linux (AL2) and so a dependency that this reference server needs: `cffi` does not match the lambda runtime when built on a Windows/ macOS machine. When the reference server is run, it might result in an error: `No module named '_cffi_backend'`. To resolve this, it is required to create a lambda layer following the steps outlined [here](https://aws.amazon.com/premiumsupport/knowledge-center/lambda-layer-simulated-docker/) and then update the speke reference lambda function to reference this layer. The `local_build.sh` and `speke_reference.json` can help you to apply this solution.
55 |
56 | ### Deploy using CloudFormation template
57 |
58 | 1. Sign in to the AWS console.
59 | 1. Choose a region such as us-east-1 or us-west-2 to start.
60 | 1. Navigate to the AWS CloudFormation console.
61 | 1. Create a new stack.
62 | 1. On the `Select Template` page, select `Upload a template file` and choose the generated `speke_reference.json` file prepared in the above section.
63 | 1. At the `Specify Details` pages, provide a stack name, like `SPEKE`.
64 | 1. Provide a value for the `KeyRetentionDays` parameter. This is the amount of time to retain a key in the S3 bucket for client playback. Keys older than this amount will be automatically removed by S3. The default is 2 days, which is usually enough for live content across multiple time zones.
65 | 1. Provide a value for the `RequiresSPEKEServerLambdaLayer` parameter. If you build and upload the `speke-libs` lambda layer zip file, set `true` to this parameter to create a lambda layer and associate it with the speke reference lambda function. Otherwise no lambda layer is created by default.
66 | 1. There are some Parameters which contain default values, this is for reference only and it is recommended that users modify this section of the reference server to return values such as playready header and pssh boxes according to their requirements.
67 | 1. The `Options` page does not require any input, although you can choose to be notified after the template completes.
68 |
69 |
70 | When the template is complete you will have an operational reference SPEKE server that can be used for HLS encryption. You can review the Resources tab of the template to see what was created or updated, and the Outputs tab for the URL of the SPEKE server and the role ARN that permits MediaPackage access.
71 |
72 |
73 | ### Limitations
74 |
75 | This solution only supports key creation for the following DRM technologies: Widevine, Playready
76 |
77 | This solution will send a blank CPIX response if the Apple Fairplay system ID is used.
78 |
79 | For Speke V2.0, this solution works for Widevine, Playready and Fairplay
80 | Due to limitations on size of environment variables provided for a lambda, users must implement their own solution to create and send PSSH, ContentProtectionData and HLSSignalingData for the different DRM systems.
81 |
82 | This solution only supports the contentProtection method to handle communication between the reference server solution and the Media Services.
83 | Users must implement copyProtectionData methods in order to handle client/player request to decrypt content.
84 |
85 |
86 |
--------------------------------------------------------------------------------
/autopep8.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # http://www.apache.org/licenses/LICENSE-2.0
4 | #
5 | # Unless required by applicable law or agreed to in writing,
6 | # software distributed under the License is distributed on an
7 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
8 | # KIND, either express or implied. See the License for the
9 | # specific language governing permissions and limitations
10 | # under the License.
11 |
12 | autopep8 -i -a -a --max-line-length 200 -r .
13 |
--------------------------------------------------------------------------------
/cloudformation/mediapackage_endpoint_common.py:
--------------------------------------------------------------------------------
1 | """
2 | http://www.apache.org/licenses/LICENSE-2.0
3 |
4 | Unless required by applicable law or agreed to in writing,
5 | software distributed under the License is distributed on an
6 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
7 | KIND, either express or implied. See the License for the
8 | specific language governing permissions and limitations
9 | under the License.
10 | """
11 |
12 | from botocore.vendored import requests
13 | import boto3
14 | import json
15 | import string
16 | import random
17 | import resource_tools
18 |
19 |
20 | def update_endpoint(mediapackage, create_endpoint, event, context):
21 | """
22 | Update a MediaPackage channel
23 | Return the channel URL, username and password generated by MediaPackage
24 | """
25 | endpoint_id = event["PhysicalResourceId"]
26 | try:
27 | result = delete_endpoint(mediapackage, event, context)
28 | if result['Status'] == 'SUCCESS':
29 | result = create_endpoint(mediapackage, event, context, False)
30 | except Exception as ex:
31 | print(ex)
32 | result = {'Status': 'FAILED', 'Data': {"Exception": str(ex)}, 'ResourceId': endpoint_id}
33 | return result
34 |
35 |
36 | def delete_endpoint(mediapackage, event, context):
37 | """
38 | Delete a MediaPackage channel
39 | Return success/failure
40 | """
41 | endpoint_id = event["PhysicalResourceId"]
42 | try:
43 | response = mediapackage.delete_origin_endpoint(Id=endpoint_id)
44 | result = {'Status': 'SUCCESS', 'Data': response, 'ResourceId': endpoint_id}
45 | except Exception as ex:
46 | print(ex)
47 | result = {'Status': 'FAILED', 'Data': {"Exception": str(ex)}, 'ResourceId': endpoint_id}
48 | return result
49 |
--------------------------------------------------------------------------------
/cloudformation/mediapackage_speke_endpoint.json:
--------------------------------------------------------------------------------
1 | {
2 | "AWSTemplateFormatVersion": "2010-09-09",
3 | "Description": "AWS MediaPackage origin endpoint configured for SPEKE HLS encryption (ID: DEV_0_0_0)",
4 | "Metadata": {
5 | "AWS::CloudFormation::Designer": {
6 | "78524ead-765b-48ea-96e7-2ede276045f3": {
7 | "size": {
8 | "width": 60,
9 | "height": 60
10 | },
11 | "position": {
12 | "x": 400,
13 | "y": 150
14 | },
15 | "z": 0,
16 | "embeds": []
17 | },
18 | "6517fd85-8aa7-4664-8437-c3ea2dc053de": {
19 | "size": {
20 | "width": 60,
21 | "height": 60
22 | },
23 | "position": {
24 | "x": 220,
25 | "y": 150
26 | },
27 | "z": 0,
28 | "embeds": []
29 | },
30 | "376667da-ea62-4513-946d-4f54a2c9a181": {
31 | "size": {
32 | "width": 60,
33 | "height": 60
34 | },
35 | "position": {
36 | "x": 20,
37 | "y": 150
38 | },
39 | "z": 0,
40 | "embeds": []
41 | }
42 | }
43 | },
44 | "Resources": {
45 | "MediaPackageResourceRole": {
46 | "Type": "AWS::IAM::Role",
47 | "Properties": {
48 | "ManagedPolicyArns": [
49 | "arn:aws:iam::aws:policy/AdministratorAccess"
50 | ],
51 | "AssumeRolePolicyDocument": {
52 | "Version": "2012-10-17",
53 | "Statement": [{
54 | "Effect": "Allow",
55 | "Principal": {
56 | "Service": [
57 | "lambda.amazonaws.com"
58 | ]
59 | },
60 | "Action": [
61 | "sts:AssumeRole"
62 | ]
63 | }]
64 | },
65 | "Path": "/"
66 | },
67 | "Metadata": {
68 | "AWS::CloudFormation::Designer": {
69 | "id": "78524ead-765b-48ea-96e7-2ede276045f3"
70 | }
71 | }
72 | },
73 | "MediaPackageEncryptedEndpointResource": {
74 | "Type": "AWS::Lambda::Function",
75 | "Properties": {
76 | "Code": {
77 | "S3Bucket": {
78 | "Fn::Join": [
79 | "-", [
80 | "rodeolabz",
81 | {
82 | "Ref": "AWS::Region"
83 | }
84 | ]
85 | ]
86 | },
87 | "S3Key": "speke/cloudformation-resources-DEV_0_0_0.zip"
88 | },
89 | "Environment": {},
90 | "Handler": "mediapackage_speke_endpoint.event_handler",
91 | "MemorySize": 2048,
92 | "Role": {
93 | "Fn::GetAtt": [
94 | "MediaPackageResourceRole",
95 | "Arn"
96 | ]
97 | },
98 | "Runtime": "python3.9",
99 | "Timeout": 300
100 | },
101 | "Metadata": {
102 | "AWS::CloudFormation::Designer": {
103 | "id": "6517fd85-8aa7-4664-8437-c3ea2dc053de"
104 | }
105 | }
106 | },
107 | "MediaPackageEncryptedEndpoint": {
108 | "Type": "AWS::CloudFormation::CustomResource",
109 | "Properties": {
110 | "ServiceToken": {
111 | "Fn::GetAtt": [
112 | "MediaPackageEncryptedEndpointResource",
113 | "Arn"
114 | ]
115 | },
116 | "ChannelId": {
117 | "Ref": "ChannelId"
118 | },
119 | "RotationInterval": {
120 | "Ref": "RotationInterval"
121 | },
122 | "RoleArn": {
123 | "Ref": "RoleArn"
124 | },
125 | "ServerUrl": {
126 | "Ref": "ServerUrl"
127 | },
128 | "StackName": {
129 | "Ref": "AWS::StackName"
130 | }
131 | },
132 | "Metadata": {
133 | "AWS::CloudFormation::Designer": {
134 | "id": "376667da-ea62-4513-946d-4f54a2c9a181"
135 | }
136 | }
137 | }
138 | },
139 | "Parameters": {
140 | "ChannelId": {
141 | "Description": "Create an encrypted origin endpoint for this MediaPackage channel ID",
142 | "Type": "String"
143 | },
144 | "RotationInterval": {
145 | "Default": "60",
146 | "Description": "Require a new encryption key every N seconds of play",
147 | "Type": "String"
148 | },
149 | "RoleArn": {
150 | "Description": "ARN of the role allowing MediaPackage to call the SPEKE server API Gateway endpoint",
151 | "Type": "String"
152 | },
153 | "ServerUrl": {
154 | "Description": "API Gateway URL of the SPEKE server that ends with the path /EkeStage/copyProtection",
155 | "Type": "String"
156 | }
157 | },
158 | "Outputs": {
159 | "MediaPackageEncryptedEndpointUrl": {
160 | "Value": {
161 | "Fn::GetAtt": [
162 | "MediaPackageEncryptedEndpoint",
163 | "OriginEndpointUrl"
164 | ]
165 | },
166 | "Description": "URL for the MediaPackage encrypted endpoint"
167 | }
168 | }
169 | }
--------------------------------------------------------------------------------
/cloudformation/mediapackage_speke_endpoint.py:
--------------------------------------------------------------------------------
1 | """
2 | http://www.apache.org/licenses/LICENSE-2.0
3 |
4 | Unless required by applicable law or agreed to in writing,
5 | software distributed under the License is distributed on an
6 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
7 | KIND, either express or implied. See the License for the
8 | specific language governing permissions and limitations
9 | under the License.
10 | """
11 |
12 | from botocore.vendored import requests
13 | import boto3
14 | import json
15 | import string
16 | import random
17 | import uuid
18 | import resource_tools
19 | import mediapackage_endpoint_common as common
20 |
21 |
22 | def event_handler(event, context):
23 | """
24 | Lambda entry point. Print the event first.
25 | """
26 | print("Event Input: %s" % json.dumps(event))
27 | try:
28 | mediapackage = boto3.client('mediapackage')
29 | if event["RequestType"] == "Create":
30 | result = create_endpoint(mediapackage, event, context)
31 | elif event["RequestType"] == "Update":
32 | result = common.update_endpoint(mediapackage, create_endpoint, event, context)
33 | elif event["RequestType"] == "Delete":
34 | result = common.delete_endpoint(mediapackage, event, context)
35 | except Exception as exp:
36 | print("Exception: %s" % exp)
37 | result = {'Status': 'FAILED', 'Data': {"Exception": str(exp)}, 'ResourceId': None}
38 | resource_tools.send(event, context, result['Status'], result['Data'], result['ResourceId'])
39 | return
40 |
41 |
42 | def create_endpoint(mediapackage, event, context, auto_id=True):
43 | """
44 | Create a MediaPackage channel
45 | Return the channel URL, username and password generated by MediaPackage
46 | """
47 | if auto_id:
48 | endpoint_id = "%s-%s" % (resource_tools.stack_name(event), event["LogicalResourceId"])
49 | else:
50 | endpoint_id = event["PhysicalResourceId"]
51 | channel_id = event["ResourceProperties"]["ChannelId"]
52 | rotation_interval = event["ResourceProperties"]["RotationInterval"]
53 | role_arn = event["ResourceProperties"]["RoleArn"]
54 | server_url = event["ResourceProperties"]["ServerUrl"]
55 | try:
56 | response = mediapackage.create_origin_endpoint(
57 | Id=endpoint_id,
58 | Description="CloudFormation Stack ID %s" % event["StackId"],
59 | ChannelId=channel_id,
60 | ManifestName="index",
61 | StartoverWindowSeconds=0,
62 | HlsPackage={
63 | "SegmentDurationSeconds": 6,
64 | "PlaylistWindowSeconds": 60,
65 | "PlaylistType": "event",
66 | "AdMarkers": "none",
67 | "IncludeIframeOnlyStream": True,
68 | "UseAudioRenditionGroup": True,
69 | "StreamSelection": {
70 | "StreamOrder": "original"
71 | },
72 | "Encryption": {
73 | "EncryptionMethod": "AES_128",
74 | "KeyRotationIntervalSeconds": int(rotation_interval),
75 | "RepeatExtXKey": False,
76 | "SpekeKeyProvider": {
77 | "ResourceId": str(uuid.uuid4()),
78 | "RoleArn": role_arn,
79 | "SystemIds": ["81376844-f976-481e-a84e-cc25d39b0b33"],
80 | "Url": server_url
81 | }
82 | }
83 | })
84 | print(json.dumps(response))
85 | outputs = {"OriginEndpointUrl": response['Url']}
86 | result = {'Status': 'SUCCESS', 'Data': outputs, 'ResourceId': endpoint_id}
87 | except Exception as ex:
88 | print(ex)
89 | result = {'Status': 'FAILED', 'Data': {"Exception": str(ex)}, 'ResourceId': endpoint_id}
90 | return result
91 |
--------------------------------------------------------------------------------
/cloudformation/resource_tools.py:
--------------------------------------------------------------------------------
1 | """
2 | http://www.apache.org/licenses/LICENSE-2.0
3 |
4 | Unless required by applicable law or agreed to in writing,
5 | software distributed under the License is distributed on an
6 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
7 | KIND, either express or implied. See the License for the
8 | specific language governing permissions and limitations
9 | under the License.
10 | """
11 |
12 | from botocore.vendored import requests
13 | import boto3
14 | import json
15 | import string
16 | import random
17 | import re
18 | import time
19 |
20 |
21 | def send(event, context, response_status, response_data, physical_resource_id):
22 | response_url = event['ResponseURL']
23 | response_body = {
24 | 'Status': response_status,
25 | 'Reason': 'See the details in CloudWatch Log Stream: ' + context.log_stream_name,
26 | 'PhysicalResourceId': physical_resource_id or context.log_stream_name,
27 | 'StackId': event['StackId'],
28 | 'RequestId': event['RequestId'],
29 | 'LogicalResourceId': event['LogicalResourceId'],
30 | 'Data': response_data
31 | }
32 | json_response_body = json.dumps(response_body)
33 | print("Response body:\n" + json_response_body)
34 | headers = {'content-type': '', 'content-length': str(len(json_response_body))}
35 | try:
36 | response = requests.put(response_url, data=json_response_body, headers=headers)
37 | print("Status code: " + response.reason)
38 | except Exception as e:
39 | print("EXCEPTION {}".format(e))
40 | return
41 |
42 |
43 | def stack_name(event):
44 | try:
45 | response = event['ResourceProperties']['StackName']
46 | except Exception:
47 | response = None
48 | return response
49 |
50 |
51 | def wait_for_channel_states(medialive, channel_id, states):
52 | current_state = ''
53 | while current_state not in states:
54 | time.sleep(5)
55 | current_state = medialive.describe_channel(ChannelId=channel_id)['State']
56 | return current_state
57 |
58 |
59 | def wait_for_input_states(medialive, input_id, states):
60 | current_state = ''
61 | while current_state not in states:
62 | time.sleep(5)
63 | current_state = medialive.describe_input(InputId=input_id)['State']
64 | return current_state
65 |
--------------------------------------------------------------------------------
/docs/behavioral-views.drawio:
--------------------------------------------------------------------------------
1 | 7Vttc6M2EP41nmk/+IZ38MfYcXI3l0zd86Tt9UtHARkzkREj5Djur68AgUHCDk4MxuPmZi6wvEn7PLur3VUG+mT1dk9AtHzEHkQDTfHeBvrtQNNUQ7PYr0SyzSSOpWQCnwQev2knmAf/Qi7Mb1sHHowrN1KMEQ2iqtDFYQhdWpEBQvCmetsCo+pXI+BDSTB3AZKlfwYeXXKpqii7C19h4C/5px2TX1iB/GYuiJfAw5uSSJ8O9AnBmGZHq7cJRInycr1kz93tuVoMjMCQNnlgeOM/ml9/M2y61Wf+0+ab84aGqp695hWgNZ/xd7hlgh+QkgAyMTu+hyEk6dEdYjPIZkO3uYo8EC9h8hFloI9fIaEBU98NCvyQySiOmHSBQzrnTyR3LQKEJhhhkr5BX5jJPyaPKcEvsHTFSn/4G0ry7IfJ+fDZV+HbXsWohboZTyFesbklc+QPGBygnKH5+WaHtzHismUJajPnKOAc84tX72BgBxyJelTule3f/8yffj48jSYLE/z+FDhwmDxfi0qGBA1weBGwSBjUINUUFtWSYSmcSQUWqy1YVAmVaehuI8pmLyqfGXuUHK5X6CFYQBSE7GwcQRKwocBEW4iLZzvZeLMMKJxHwE0e3TCHymRLumKDvFXZIfNxFLBHSHGOEIji4LkAkEB3TeLgFf6AceZKEyle0+RLk8JFKqdByKkipCkyQoWfLCNUWNPpDUdCaD6bfp8y0c3sG/t/GnoRDtiErxIvze4dYJYEmARNSaEpdukQzPHAvBUQwoQusY9DgMoYyXo7yJzm7sms6NKuU6WsSd1pS5NyIM+p/wBWzx5IwsQ6dLPYcY3sN7TesX/UF/brRyuzyv5RQ/YbbWnSkNkPXQKZujTlEYRskX6lMdpSesd6VY7SZ6K9caw2BdqrWjPet+b0TUmTWZ4wXrsv8EpXObbVP8bbfWG8eaw2BcbrtcrskPF1mrxzcbSdEUwhX94ovySkZJoYsndp7EPKC9yy418PaV1MkZ8xpXjFLsDQu0kqSYkMYfclFbG5/MU5m578fI/AMV4TFx4iSXYfBcSHhyDkS2boVQpXMoAEIkCZ0VVGUYcHf3SWJUZFsiAul2wB0mxCsyKdKqNaDOPjQDsS0Cycr0mK7kvi5CbpHHmVargm6NPo4giyS+OicKJml3lR0knO3gJawM6OU9S/jMxPAW81BF49D/C6YVVfkQ20NeBzd1JC3s/KXjAz5D4bsdYQy1E3WBqqYMRiiGvZiFW5ZJZb8UWa6qghvNp54NWtjk1VLjokekm7MUX4VTy2THnNi9YeoKDPBqw3dcYdQWwKRVZd69iC5cz6oi04x+1dgPUz4Tvq2ITlCmxqr0msDeEmj7j7VtTZcZzWWoaJbcuLsESDeZNpl7tMd9JyMgk4YxBc0KTxxLLOIPQf0rNbrd4NELwOvaLT1b7pd8UMu5p6GQ0tn6kHbEu38Wxy/3eE9Z7hCH3k9+6v9p3ZQTaCPU+bwtNiSbBtvst5RUwxuYClZePIZHfDT1tAUmqHtx2Z5Or5ZUcmu1/+R8LX6TYyaXIe+H9k6gUzTHv0pRqbii5Dy7Ep/07T2GTZR8UmeV5i0atlzhtyQvVH4EHMRBMcxuvVtbaxhFWy49SVoetq+qfoONbvnJOQOhR3Pl7Sf2dr2wEaNa3p12ysqm3emidQZf1w5RwzZ/2Vb9gRtyaen/Zan2lvHEP7hnsW2nMg8vrmUlWpq2YzXZpteRC5EZ7vfsoa4tftR/RR7/xIVx3xD5HfPIb8Ws1+8U4dibxm5G3RJFOKENiiIJaJ31JlQ8g9qknLoYjykbxo3zrnxB3QUXut71qN9LjkLsFbxuhIeN9NZzuCV1Xa63DXqkQOVmfxfYdjhqqLWmqYJ7Tm5+ROBZtNfNDJdVzhKZyjeirnaDaG7ZNGoerC1kZFxPFEJR3VkqzvQI3mVCZXu5soj5EdFv6PZ4Ddu/AoriZVtb2edK1Kelz4Pz4+7sf3XPFRxre9nnR9Zly7ayi31hj6K1iTyfXGYvOg2COT1VRThFRIxVs22Zo9/xdsswcQPtuiVkRYs4V3fNho2enur9Sz23d/669P/wM=
--------------------------------------------------------------------------------
/docs/bv-key-generation.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/speke-reference-server/5dc9c382532e5c3786ebae3fe540d5ff6ecb6de5/docs/bv-key-generation.jpg
--------------------------------------------------------------------------------
/docs/bv-key-retrieval.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/speke-reference-server/5dc9c382532e5c3786ebae3fe540d5ff6ecb6de5/docs/bv-key-retrieval.jpg
--------------------------------------------------------------------------------
/docs/deployment-view.drawio:
--------------------------------------------------------------------------------
1 | 7Ztfc5s4EMA/TR7DIIQQPMaO3ctdM/VcpnM3ffHIINtcBOJAzp/79CcZQQA5jeMa3Na1ZxKzEouk/WmlXdkXcJw8fchJtr7lEWUXjh09XcDrC8dxfB/Kf0ryXEqAjVEpWeVxpGUvgrv4P1pV1NJNHNGiVVFwzkSctYUhT1MaipaM5Dl/bFdbctZ+akZW1BDchYSZ0r/iSKy1FNj2S8FvNF6t9aN9pAsSUlXWgmJNIv7YEMHJBRznnIvyU/I0pkyNXjUu5X3TV0rrhuU0FfvcIP6+vH/+AuPsX/b58gu5vYXp75daywNhG93hm6tbKbij+UMcUt1y8VwNB98IFqd0XI+2fQFHS56KMWc839aB8j1VTRitchLFtFU2RS52g0bZdZxLRTFPZXnKczVmo2XMWOOeMQYQyF6NCpHze9ooWW5fsiQixZpGujkPNBextOBHsqBsxotYq19wIXjSqHDF4pUqEDyTUqKvQtkqKh8wWouEyWuge6jZBE51rUdFPZIUWTkcy/hJtWMkjZ2pwuRppSaGRR4L18ppwTd5SG9C1Z6RvCw/tWtJ3lMRi+c5SaM5CUNaFPOEpJKzZGvpkTaZ7AR9epUFUBMm5yblCRX5s6yib3ArfPW09PXl4wvi2C9F6wbclYzoSbWqFb9wJz9o9N6BITQwvJrdSMEHIugjee4RyMBF11PnfUCiK2iP0NkASbJ4vtKG6IW++vpU+LkGfh9JsohIn44Q+xPbfR931zYaA3w23LHSBr0gJwWW3XzB0xKIDALHjG+iaS7H95f3OyWFobLDcmuHXkhE3omdHzbQu4M9IufZVxDi9yHnYAyAdzbIFbAf1LBzYtSqoK7J2mzyx0SK/qRLKvulgNuiJ8f/qOSlPKUmWKMxgMgES1fei6kkjiLVQoOpuqDCKi9H+Juoynis2Jw8yK4VWslO0gRNMiY3bbKcdVrM6PJIvsx/ex/noSH5MsPZWxrFZEbCexXnOx5TPV9ItryV+lTRN0mj7cj+IMwN6ccOIe74aPk7ItRB0XLMENWApTHMNI2uVPZJGYvx8F6J2GJ7XdlsSwDJRVVPAyDvnMas0mMseMhFI0/FDTnfpFFNStkWGhnJrM54y/ZuF6A9nLRs24qKr1XcbcCGheosWU4lGfFDu3G7bKTVzcrJWMHg4DYNoEqyVSrKTs3qKTx9RRF8S1HZaUPRFpm6j99AkbnZuqYZ42rcRpvwng7igV7ZTv3wHmixHcH5YyzWc774R2opjuOMPPf90eOgzsk3sKoTGCSN5N8ynTbmkdpffbmZGZi9aeiGBXfbvISpSpKDXRTsZLnF284aO4wNI0tCTXduvLsu0wnU+0jLUmddkiG0FTRf2CAB2tauPbZteT3RAM1d9k+6VFXcv7lUlX53kLUK2qhFiOdiC3jIARgGANouPGzlQlVL6rSBazWUusGg6xh0zgYxvCdi7mCEARu0nRDE2HI9H/guxNBxIfAOY8z32+hCB1mOB3zfxzZAAONhGTOz8u1g7ldu/nQpKsq2p5GEzRNlk0zbpJ/DyZOfTprZ+Z/U2+0d+w3n7S5B2ykB/8DYz+ls4g1Fffsz7xdFu479B6HIczqnLu6BFPkYfV1R3xSZGYRzpwgNRhFqQ+R5h6ahujFCV1HfEJn5gnOHaLgA0T2SJ3KCE3ui4GwgAntCBAcMApHbSjyhNgyub1s+cgLfcwB2UXdrvC9jrh187SkY2hbGngs8BAMb2d6gALrmad/nQsWFnzYi24jCoLGBIM9oWqLVjJjqCA20I6tTsATst1nyjpSy6pyKIHigS6pvfE1R30SYh3SfMpoTIX1FF4Yq+N0k7CpUFQ6OyQ1Qvin6vXQ6g+ghM/ytqjTD3+4e4njfjjQTMdWh1Xc4wSoGvqcJ1t04Hrzmu+ANRX1PMDMT8lOgsMdR9pFQkM/qZLfAgSwEnS/fYGgFvvQCvhcgDzlHW4zl5cuPVMrqL7/1gZP/AQ==
--------------------------------------------------------------------------------
/docs/deployment-view.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/speke-reference-server/5dc9c382532e5c3786ebae3fe540d5ff6ecb6de5/docs/deployment-view.jpg
--------------------------------------------------------------------------------
/docs/logical-view.drawio:
--------------------------------------------------------------------------------
1 | 5Vtdj9o4FP01vKzEKP5IgEeG+Wi1rVR1NK3UlyqAAUtJnA2Ggf76tRMbEjvdyTKZxNOZB0iurx187vGxfZ0ZoFl8uM/CdPOZLUk0gN7yMEA3AwgBhoH4kpZjYRkHXmFYZ3SpnM6GB/qLKKN229El2VYcOWMRp2nVuGBJQha8YguzjD1V3VYsqj41DdfEMjwswsi2fqdLvlFW4Hnngg+Erjfq0WNfFcShdlaG7SZcsqeSCd0O0CxjjBdX8WFGIgmexqWod/eb0tMPy0jCm1R4fDpMH+mP7NvDz1vyA6bfk8lsCItW9mG0Ux2+TRbZMeUsU7+aHzUUogOpvNzF0XQhHdD1nmScCrA+hXMSfWFbyilLhMuccc7iksM0omtZwFkqrBseR+IGiEu7F6pjsiY5lEyqV/eExYRnR+GiSrGGXFNM3T6d44WUaVOKlCZiqBiyPjV8BlFcKBz/B6bIwvSb4DATphlLtruYvB1oIfQr0CKvZ2x9C9sBDCLx1Ou0gmjwz06Oq+s4zNZUoDMVpV56EJ85MF5hH+aYyTJcKhPY8GFYoCrLFgI/GTPdprhaq+/8yXNteDjGspd0ISr9TY66XHR0btYRtvRsM9hQH1n1i24ispK1mPBaRbmgrKgE8XrFEq4EFEB1fxfGNJLB+0CiPZGtts+RcXX4wRqOANwlSUYukyQnhvfx5p2ww/dcY8fYZXaIGYJLsN8RQ0bQNYZMXGZIoR/3JCFZmC8JtEOmPT6HoiUaRu+EPxPsGn9Q3QRkYEyS5VRuT8RdwhLSEJUt22UL8ry4ccE8wp+fIsmysvuxMc5IJFi2r26G6gBTVb8wmquXjhwA1eDgkQF60SNVrbx7MVoKjCgjYDRUdNlqSIAcHktuqXTYWiE+dfkFUbcnFmCHXQCer+hFlDOypb/CeV7kVQlgjDVzQJ72ADZR/pOQapOsnjk4bU2bD7ah6tILeTEE1X2F3mboFthqtSX8VaJki/tfL41SVmiKQ2FqJ0pgcjWp/AU9BU2P/R4EddRQUP1uBPWknzoGkwv1FBurrtPO3hk9xcAKuit6qvnojp5aa+jOhqadQXRGT9sLUzd62l3Q7BRlV3o6aain42701J9UB45vymBTPR0bWTBkRrN/PcXu6ilyTU/N9Wl3Q9POcL84Sq3paWth+tP0NKjRU2cyRl/lmQTZy4SQ9/j10ztJCwWuZYVwf1khzU9XdjHAPBTAF8660Egvnc6i3Jl13c0KYdeyQmYwu8sv2EkhdyZdx5JCQ+BdeZU/3FPUgrqXAZ4VVGG5yyemPHDirjw3taK2sKHYNt3ilGcsPTu1nUbyLxTgAD3T0G8EuDUO1K283hAHmp7DvAIHTLnFclzjAPhonH/6wWWUwP7kyguMF4dAAK8C2C01LlpvvT41/IbUgP1RwzfkAXmwyo0LqYEMyoGGSZLWKFH37ocDlGi8Pu+RE9A4gMXmAWxTEvijGn3AEHeuD3WvebwlMqAeBcKcOy49hkLInIS6VQQtdK6RoOkk0ScHjE28f+kmfoxqBMH3hSCAUm6vJVqI2/Mr94X7+R8X0O2/
--------------------------------------------------------------------------------
/docs/logical-view.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/speke-reference-server/5dc9c382532e5c3786ebae3fe540d5ff6ecb6de5/docs/logical-view.jpg
--------------------------------------------------------------------------------
/docs/physical-view.drawio:
--------------------------------------------------------------------------------
1 | 7ZpLc6M4EIB/jY+b4g0+xo49m82kylWpmt2bSwYZayIQK0Rsz69fyQgMkjN2HExS3iSHQKtpWt0fenQ0sMfJ5hsF2eqRRBAPLCPaDOy7gWWZjuXxP0KyLSWBZ5SCmKJIKu0FT+gXlMJKrUARzFuKjBDMUNYWhiRNYchaMkApWbfVlgS335qBGGqCpxBgXfo3ithKSk3D2Df8CVG8kq8OXNmQgEpZCvIViMi6IbInA3tMCWHlVbIZQyyCV8WlfG76SmvtGIUpO+WBv+iv1EsfwuJhkUb3/6TjBfrxh1VaeQG4kB3+weNNuGhM0rxIIJXOs20VEd6PTFwWCb4NGaEDe/QCKUM8Zt/BAuIZyRFDJOUqC8IYSRoKtxjFooGRjEtXLMH8xuSX0g2uBjev9s+so8ZxgySBjG65inygAkuC5njSwnqfNluqrBoJqx4DEpS4NryPJb+Q4XxDaG0ttLezey74Bhhcg60WV1IwjFI4rkE2eFyWJGVjgkWUuY7Nf6fChVFMQYRgq23qOr4zbLTdIcoNlZlICRVBGC0Rxo1nxr5pm7yTo5xR8gwbLcvdD2+JQL6CkXTnzEQDeRdyryBtZ170UH72plXdy6iIV4I8K8OxRBvhx6jiL9nEYsi5AevcuaEwJwUN4X0o/Bnx2/KqrQUyNI9l+LthzmlDZ7s6dH6gQ1fJOofO0aB7mk0eJgPLw/zVowX/nL1YXH0HySICXHVapCUknfKYkhTquN0Z7tj0Ndyk8kmklUCpmNX8dUpaRpCwMnnhxnJp5CB9eBfM+bIKpT3Citu1Px1AF1ifDDpXg+4BCn+f+OwgptceyLJ83zS9d5HV5xh2MlmLInyGbL5GbDUni5/cSt4VROaN28LIPYSR2yNGnj52wZBC3mPLeAQpJ0lfjHxNmhefNPMyCfNEpqAT/Pj62fpcg5iv0TfGpIimlIe2c+yGjns3td6GnXtrGyP3f4NdKKK/3EW/E+KsoD1rHhzu+gQu0IB7hBECMxA+7yZNdcU2SaPdtNH9GOgHE8N5G4yvrOSuFUaIYcJdAHieiBxlMkedgOl4n2w5N/waCT8VfJceCT8cuKrW1yCO2wlJtp1RwuDh7WkjGTCNbkWpT6QUk/BZiPBid19ldgcLoKzSkxsD/uQU4cqOhpPruCNPjIyUFGlU81T6AiOtcqhkgPu7S+/xSYC7FkP2Gz37cEYbGasrkhRiwNBL27dDOZPmZuWkUg9HpkLHUCmQlX2a1VPR9BVDgTKueb7iS9lpzdAOobqP76DK1KgiFMXoWlHyT0TJ7Q2lwFAIcM5EyTTMI5YuzZJeJ3/elThCEK70Asd1AOV8OqBMpfLlBuZ5QNn2EUOX5kn/58CVQmSfCJHzYROca5wJkafSqBq6NER6sf9KITp1JPJ6g8hVpjbXORMiX6VRNXRpiPTifTmzUfhvAXN9w3cdRFknEuX3N7e5yhLH88+c2xRDTt9rJb2Of9Xr7uGJKAW9oWRpq+XheSg51hFDl0ZJL8q/yOMpX8NTzV4/ezkVBfXczOl7OfuIpUtDpdc3J2lIt5k4y6Ti9IbDTkdOJjTAfPVwgFZxlJ6+q7wcOK1w1yY+6lxUBdLvlq0HPt26omu2PlCjvGvWYrVjZb1W6swTvkmvo53MUPmSzt7JqGuPznYy/HZ/wrFU358TtSf/AQ==
--------------------------------------------------------------------------------
/docs/physical-view.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/speke-reference-server/5dc9c382532e5c3786ebae3fe540d5ff6ecb6de5/docs/physical-view.jpg
--------------------------------------------------------------------------------
/docs/software-architecture.md:
--------------------------------------------------------------------------------
1 | # SPEKE Server Architecture Views
2 |
3 | ## Views
4 |
5 | This document describes the architectural views for the open source SPEKE Reference Server.
6 |
7 | The views used in this document are:
8 |
9 | 1. use case view
10 | 2. logical view (E-R, types)
11 | 3. deployment view (deployment artifacts to services)
12 | 4. physical view (deployed code, configured services, communication paths)
13 | 5. behavioral views (sequence diagrams)
14 |
15 | ### Use Case View
16 |
17 | 
18 |
19 | #### Actors and Use Cases
20 |
21 | **Actor: Encryptor**
22 |
23 | *Use Case: Generate Encryption Key*
24 |
25 | 1. The actor sends a request that includes the content ID and key ID
26 | 2. The server determines if key generation material for the content ID exists
27 | 1. If not, random key generation material is created and stored in Secrets Manager under the content ID
28 | 2. If so, the key generation material is retrieved
29 | 3. The new key is generated and stored
30 | 5. The new key, content ID and key ID is included in the response to the actor
31 |
32 | **Actor: Video Consumer**
33 |
34 | *Use Case: Retrieve Decryption Key*
35 |
36 | 1. The player retrieves the current version of a playlist
37 | 2. The player parses the playlist to determine the next key to retrieve
38 | 3. The player requests the key data from the key URL in the playlist
39 | 4. The server returns the key data to the player
40 |
41 | **Actor: Operator**
42 |
43 | *Use Case: Configure Encryptor*
44 |
45 | ### Logical View
46 |
47 | 
48 |
49 | The elements in this view represent the types (and terminology) of the problem space.
50 |
51 | #### Retrieval URL
52 |
53 | This type represents a unique URL used to retrieve a specific key.
54 |
55 | #### Symmetric Key
56 |
57 | This type represents a block of data used to encrypt and decrypt segments of video.
58 |
59 | #### Key ID
60 |
61 | This type represents a unique index value for a key within a content ID.
62 |
63 | #### Content ID
64 |
65 | This type represents a live stream or video on demand playback.
66 |
67 | #### Key Generation Material
68 |
69 | This type represents a block of data known only to the SPEKE server and related to a specific content ID. This data is used with other information provided by the encryptor to generate new keys.
70 |
71 | ### Deployment View
72 |
73 | The deployment view shows the relationships between the deployment units (installers, binaries, CloudFormation templates) and the target services that receive the deployment configuration or run deployed code.
74 |
75 | 
76 |
77 | The above diagram shows the SPEKE server template used to install IAM resources, Lambda code, and configure API Gateway, CloudFront and S3. The Lambda deployment archive is hosted on several buckets in different regions to simplify installation for end users.
78 |
79 | An optional template is provided to quickly configure a encrypted MediaPackage origin endpoint. This template uses outputs from the SPEKE stack and a MediaPackage channel name to create a new endpoint.
80 |
81 | ### Physical View
82 |
83 | The physical view shows the deployed software with configured cloud resources and control and data connections among them.
84 |
85 | 
86 |
87 | This preceeding diagram shows the resources that participate in the encryption and decryption process of SPEKE with MediaPackage. The SPEKE server Lambda function receives requests from MediaPackage through API Gateway for encryption keys. MediaPackage builds the playlist and encrypted video segments and provides them through one CloudFront distribution. The playlist entries for decryption keys include a URL to retrieve each key through the second CloudFront distribution.
88 |
89 | ### Behavioral Views
90 |
91 | The following sequence diagram represents the process for requesting a new key, generating it, storing it and returning it to the Encryptor for use.
92 |
93 | 
94 |
95 | The following sequence diagram represents the process for requesting and using key data retrieved from the URL specified in the video playlist.
96 |
97 | 
98 |
99 |
--------------------------------------------------------------------------------
/docs/use-cases.drawio:
--------------------------------------------------------------------------------
1 | 7ZjbctowEIafhst2fAYuOYVOD9M06TTNpbA3tmZsyyNksPv0XdXyCdFASSllJldoVyvZ++1vrYaBPUuKJSdZ9IkFEA8sIygG9nxgWaY58vBHesrKMxzZlSPkNFBBreOe/gDlNJQ3pwGse4GCsVjQrO/0WZqCL3o+wjnb9sOeWNx/akZC0Bz3Pol17wMNRFTnZRjtxDugYaQePXLVRELqYOVYRyRg247LXgzsGWdMVKOkmEEs4dVcqnU3v5ltXoxDKo5ZcFes3j9s0i+L/OswK5zldLry36hibEicq4QXqc/LTDCu3lqUNQpMIJPDPIknvgywpxvggiKsj2QF8S1bU0FZiiErJgRLOgGTmIZyQrAMvZFIYjRMHOpZqMTkSig6LpXVElgCgpcYomZtTxFWErOHyt62BbOVK+qUql5GlETCZueWIg4UyD+A6mhQv6GIGbpmLF3nCVwv2wbkpdi6GtslpMCJAPQq7UpOlvEBSg0zPgAPDjSm24gKuM+IL2e2eHb9dXSeuSNLz9XQmc4edsNzsfM0dne4L4WNZDeH/5idZV2a3VBjpwNKg4nsOGixDPCLnKLnhsY1F7RUfzOtI4mtWc59eOa11PktCA9BHP5sIOh1O51/l2/9DXOIiaCbfkPcB1htd8soptHU0hn2a+k4OzWqklSrug1sZyPPPrBRRUHb6Fe9mxxPl8DonBLAyvPyOxpGbTxK461bm/OiOzkvlXW6dJwjpeNdUDqjnSPUOFU6Oxq03PE/lc5Yk87nTDatK75lueaFbwL18ztQ8Xr1RMOcd+4CewhfspG5xqUbmWlq2K76GBsfeYzVhXhtgS8Rj3VW8RRUNNrB8WNn3OpGGi+XTS2Hg7qxX2XzjGzQbP/BqMLb/4HsxU8=
--------------------------------------------------------------------------------
/docs/use-cases.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/speke-reference-server/5dc9c382532e5c3786ebae3fe540d5ff6ecb6de5/docs/use-cases.jpg
--------------------------------------------------------------------------------
/images/speke-reference.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/speke-reference-server/5dc9c382532e5c3786ebae3fe540d5ff6ecb6de5/images/speke-reference.png
--------------------------------------------------------------------------------
/images/speke-s3-cache.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/speke-reference-server/5dc9c382532e5c3786ebae3fe540d5ff6ecb6de5/images/speke-s3-cache.png
--------------------------------------------------------------------------------
/lambda/speke_libs/.gitignore:
--------------------------------------------------------------------------------
1 | python/
2 |
--------------------------------------------------------------------------------
/lambda/speke_libs/requirements.txt:
--------------------------------------------------------------------------------
1 | cffi==1.15.1
2 |
--------------------------------------------------------------------------------
/local_build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # http://www.apache.org/licenses/LICENSE-2.0
4 | #
5 | # Unless required by applicable law or agreed to in writing,
6 | # software distributed under the License is distributed on an
7 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
8 | # KIND, either express or implied. See the License for the
9 | # specific language governing permissions and limitations
10 | # under the License.
11 |
12 | ORIGIN=`pwd`
13 | BUILD=$ORIGIN/build
14 |
15 | # date stamp for this deploy
16 | STAMP=`date +%s`
17 | echo build stamp is $STAMP
18 |
19 | # create a build folder
20 | mkdir $BUILD
21 |
22 | # clear the build folder
23 | rm -f $BUILD/*
24 |
25 | # create the reference server zip with a unique name
26 | SERVZIP=speke-reference-lambda-$STAMP.zip
27 | cd $ORIGIN/src
28 | # using zappa for help in packaging
29 | zappa package --output $BUILD/$SERVZIP
30 |
31 | # create a lambda layer zip with a unique name if required
32 | # - https://github.com/awslabs/speke-reference-server#sidenote-building-the-lambda-on-macwindows
33 | # - https://aws.amazon.com/premiumsupport/knowledge-center/lambda-layer-simulated-docker/
34 | if [ "${REQUIRES_SPEKE_SERVER_LAMBDA_LAYER}" = "true" ]; then
35 | cd $ORIGIN/lambda/speke_libs
36 | docker run \
37 | -v "$PWD":/var/task \
38 | "public.ecr.aws/sam/build-python3.9" \
39 | /bin/sh -c "pip install -qq -r requirements.txt -t python/lib/python3.9/site-packages/ -U; exit"
40 | zip -r $BUILD/speke-libs-$STAMP.zip python > /dev/null
41 | fi
42 |
43 | # create the custom resource zip with a unique name
44 | RESZIP=cloudformation-resources-$STAMP.zip
45 | cd $ORIGIN/cloudformation
46 | zip -r $BUILD/$RESZIP mediapackage_endpoint_common.py mediapackage_speke_endpoint.py resource_tools.py
47 |
48 | # update templates with the new zip filenames
49 | sed -e "s/DEV_0_0_0/$STAMP/g" speke_reference.json >$BUILD/speke_reference.json
50 | sed -e "s/DEV_0_0_0/$STAMP/g" mediapackage_speke_endpoint.json >$BUILD/mediapackage_speke_endpoint.json
51 |
52 | cd $BUILD
53 |
54 |
--------------------------------------------------------------------------------
/misc/sync_commands.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """
3 | http://www.apache.org/licenses/LICENSE-2.0
4 |
5 | Unless required by applicable law or agreed to in writing,
6 | software distributed under the License is distributed on an
7 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
8 | KIND, either express or implied. See the License for the
9 | specific language governing permissions and limitations
10 | under the License.
11 | """
12 |
13 | import boto3
14 |
15 | # this is a tool to generate all the s3 sync commands for mediapackage regions
16 |
17 | bucket_base = "rodeolabz"
18 | mediapackage_regions = boto3.session.Session().get_available_regions(service_name='mediapackage')
19 |
20 | for region in mediapackage_regions:
21 | print("aws s3 sync . s3://rodeolabz-{region}/speke/ --acl public-read --delete".format(region=region))
22 |
--------------------------------------------------------------------------------
/pylint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # http://www.apache.org/licenses/LICENSE-2.0
4 | #
5 | # Unless required by applicable law or agreed to in writing,
6 | # software distributed under the License is distributed on an
7 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
8 | # KIND, either express or implied. See the License for the
9 | # specific language governing permissions and limitations
10 | # under the License.
11 |
12 | # ignore line too long problems (C0301)
13 |
14 | find . -iname '*.py' -print0 | \
15 | xargs -0 pylint -d C0301
16 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | argcomplete==3.1.1
2 | asn1crypto==1.5.1
3 | astroid==2.15.6
4 | awscli==1.29.37
5 | boto3==1.28.37
6 | botocore==1.31.37
7 | certifi==2023.7.22
8 | cffi==1.15.1
9 | cfn-flip==1.3.0
10 | chardet==5.2.0
11 | click==8.1.7
12 | colorama==0.3.9
13 | cryptography==41.0.3
14 | docutils==0.15.2
15 | durationpy==0.5
16 | Flask==2.3.3
17 | hjson==3.1.0
18 | idna==3.4
19 | importlib-metadata==6.8.0
20 | isort==5.12.0
21 | itsdangerous==2.1.2
22 | Jinja2==3.1.2
23 | jmespath==1.0.1
24 | kappa==0.6.0
25 | lambda-packages==0.20.0
26 | lazy-object-proxy==1.9.0
27 | MarkupSafe==2.1.3
28 | mccabe==0.7.0
29 | pip-tools==7.3.0
30 | placebo==0.8.1
31 | pyasn1==0.5.0
32 | pycparser==2.21
33 | pylint==2.17.5
34 | python-dateutil==2.8.2
35 | python-slugify==8.0.1
36 | PyYAML==6.0.1
37 | requests==2.31.0
38 | rsa==4.7
39 | s3transfer==0.6.2
40 | six==1.16.0
41 | text-unidecode==1.3
42 | toml==0.10.2
43 | tqdm==4.66.1
44 | troposphere==4.4.1
45 | typed-ast==1.5.5
46 | Unidecode==1.3.6
47 | urllib3==1.26.10
48 | Werkzeug==2.3.7
49 | wrapt==1.15.0
50 | wsgi-request-logger==0.4.6
51 | yapf==0.40.1
52 | zappa==0.58.0
53 | zipp==3.16.2
54 |
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/.gitignore:
--------------------------------------------------------------------------------
1 | .pytest_cache
2 | reports
3 | .idea
4 | spekev2_requests/test_case_*
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | url = "https://pypi.org/simple"
3 | verify_ssl = true
4 | name = "pypi"
5 |
6 | [packages]
7 | requests = "*"
8 | aws-requests-auth = "*"
9 | pytest = "*"
10 | boto3 = "*"
11 | pytest-html = "*"
12 | m3u8 = "*"
13 |
14 | [dev-packages]
15 |
16 | [requires]
17 | python_version = "3.9"
18 |
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/Pipfile.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "hash": {
4 | "sha256": "b6b53e1ef1814b2a2c6172664d63ff33d00424fdc5777fe58d7879971ef9c33d"
5 | },
6 | "pipfile-spec": 6,
7 | "requires": {
8 | "python_version": "3.9"
9 | },
10 | "sources": [
11 | {
12 | "name": "pypi",
13 | "url": "https://pypi.org/simple",
14 | "verify_ssl": true
15 | }
16 | ]
17 | },
18 | "default": {
19 | "attrs": {
20 | "hashes": [
21 | "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6",
22 | "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"
23 | ],
24 | "markers": "python_version >= '3.5'",
25 | "version": "==22.1.0"
26 | },
27 | "aws-requests-auth": {
28 | "hashes": [
29 | "sha256:33593372018b960a31dbbe236f89421678b885c35f0b6a7abfae35bb77e069b2",
30 | "sha256:646bc37d62140ea1c709d20148f5d43197e6bd2d63909eb36fa4bb2345759977"
31 | ],
32 | "index": "pypi",
33 | "version": "==0.4.3"
34 | },
35 | "boto3": {
36 | "hashes": [
37 | "sha256:5de0db8433acc06c1b6811899587d65997fb4031f54506e63716c9e188b4ff3c",
38 | "sha256:b50067fc63c519387fc3ec46c05a78e5c7e25c1a1ec0d07a40103c4a47544fd4"
39 | ],
40 | "index": "pypi",
41 | "version": "==1.17.103"
42 | },
43 | "botocore": {
44 | "hashes": [
45 | "sha256:6d51de0981a3ef19da9e6a3c73b5ab427e3c0c8b92200ebd38d087299683dd2b",
46 | "sha256:d0b9b70b6eb5b65bb7162da2aaf04b6b086b15cc7ea322ddc3ef2f5e07944dcf"
47 | ],
48 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
49 | "version": "==1.20.112"
50 | },
51 | "certifi": {
52 | "hashes": [
53 | "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3",
54 | "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"
55 | ],
56 | "index": "pypi",
57 | "version": "==2022.12.7"
58 | },
59 | "chardet": {
60 | "hashes": [
61 | "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
62 | "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
63 | ],
64 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
65 | "version": "==4.0.0"
66 | },
67 | "idna": {
68 | "hashes": [
69 | "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
70 | "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
71 | ],
72 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
73 | "version": "==2.10"
74 | },
75 | "iniconfig": {
76 | "hashes": [
77 | "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
78 | "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
79 | ],
80 | "version": "==1.1.1"
81 | },
82 | "iso8601": {
83 | "hashes": [
84 | "sha256:32811e7b81deee2063ea6d2e94f8819a86d1f3811e49d23623a41fa832bef03f",
85 | "sha256:8400e90141bf792bce2634df533dc57e3bee19ea120a87bebcd3da89a58ad73f"
86 | ],
87 | "markers": "python_version < '4.0' and python_full_version >= '3.6.2'",
88 | "version": "==1.1.0"
89 | },
90 | "jmespath": {
91 | "hashes": [
92 | "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9",
93 | "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"
94 | ],
95 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
96 | "version": "==0.10.0"
97 | },
98 | "m3u8": {
99 | "hashes": [
100 | "sha256:3ee058855c430dc364db6b8026269d2b4c1894b198bcc5c824039c551c05f497",
101 | "sha256:7dde0a20cf985422593810006dd371a1e3e7afd33a76277111eba3f220288902"
102 | ],
103 | "index": "pypi",
104 | "version": "==0.9.0"
105 | },
106 | "packaging": {
107 | "hashes": [
108 | "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3",
109 | "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"
110 | ],
111 | "markers": "python_version >= '3.7'",
112 | "version": "==22.0"
113 | },
114 | "pluggy": {
115 | "hashes": [
116 | "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
117 | "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
118 | ],
119 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
120 | "version": "==0.13.1"
121 | },
122 | "py": {
123 | "hashes": [
124 | "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719",
125 | "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"
126 | ],
127 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
128 | "version": "==1.11.0"
129 | },
130 | "pytest": {
131 | "hashes": [
132 | "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b",
133 | "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"
134 | ],
135 | "index": "pypi",
136 | "version": "==6.2.4"
137 | },
138 | "pytest-html": {
139 | "hashes": [
140 | "sha256:3ee1cf319c913d19fe53aeb0bc400e7b0bc2dbeb477553733db1dad12eb75ee3",
141 | "sha256:b7f82f123936a3f4d2950bc993c2c1ca09ce262c9ae12f9ac763a2401380b455"
142 | ],
143 | "index": "pypi",
144 | "version": "==3.1.1"
145 | },
146 | "pytest-metadata": {
147 | "hashes": [
148 | "sha256:acb739f89fabb3d798c099e9e0c035003062367a441910aaaf2281bc1972ee14",
149 | "sha256:fcc653f65fe3035b478820b5284fbf0f52803622ee3f60a2faed7a7d3ba1f41e"
150 | ],
151 | "markers": "python_version >= '3.7' and python_version < '4.0'",
152 | "version": "==2.0.4"
153 | },
154 | "python-dateutil": {
155 | "hashes": [
156 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
157 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
158 | ],
159 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
160 | "version": "==2.8.2"
161 | },
162 | "requests": {
163 | "hashes": [
164 | "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
165 | "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
166 | ],
167 | "index": "pypi",
168 | "version": "==2.25.1"
169 | },
170 | "s3transfer": {
171 | "hashes": [
172 | "sha256:9b3752887a2880690ce628bc263d6d13a3864083aeacff4890c1c9839a5eb0bc",
173 | "sha256:cb022f4b16551edebbb31a377d3f09600dbada7363d8c5db7976e7f47732e1b2"
174 | ],
175 | "version": "==0.4.2"
176 | },
177 | "six": {
178 | "hashes": [
179 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
180 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
181 | ],
182 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
183 | "version": "==1.16.0"
184 | },
185 | "toml": {
186 | "hashes": [
187 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
188 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
189 | ],
190 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
191 | "version": "==0.10.2"
192 | },
193 | "urllib3": {
194 | "hashes": [
195 | "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc",
196 | "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"
197 | ],
198 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
199 | "version": "==1.26.13"
200 | }
201 | },
202 | "develop": {}
203 | }
204 |
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/README.md:
--------------------------------------------------------------------------------
1 | ## Setting up an env and invoking the test suite
2 |
3 | 1. Install a pipenv environment
4 | - Reference: https://pipenv-fork.readthedocs.io/en/latest/install.html
5 | 2. Navigate to the test suite folder and run: `pipenv install`
6 | 3. Setup credentials to invoke the AWS resources using: https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html
7 | 4. Run the test suite using: `pipenv run pytest --speke-url <>`
8 | 5. The test suite generates a report with name as: `report_timestamp.html` under a new folder named `reports`.
9 |
10 | ## Additional notes
11 | - The test suite generates xml request files under `spekev2_requests`. This step is run every time the test suite is invoked.
12 | - Existing folders and files are deleted if present and new ones are generated.
13 | - To skip this step (when re-running the test suite, for example), use `--skip-artifact-generation`.
14 | - To test on VOD suite, use `--test-vod`.
15 | - Usage: `pipenv run pytest --speke-url <> --skip-artifact-generation`
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/__init__.py:
--------------------------------------------------------------------------------
1 | # Intentionally left empty
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/conftest.py:
--------------------------------------------------------------------------------
1 | import os
2 | import datetime
3 | import pytest
4 | from .helpers.generate_test_artifacts import TestFileGenerator
5 |
6 |
7 | def pytest_addoption(parser):
8 | parser.addoption("--speke-url", help="Speke Key provider URL")
9 | parser.addoption("--skip-artifact-generation", help="Skip generation of test artifacts", action='store_true')
10 | parser.addoption("--test-vod", help="Generated request won't contain ContentKeyPeriodList", action='store_true')
11 |
12 |
13 | @pytest.fixture(scope="session", autouse=True)
14 | def spekev2_url(request):
15 | """
16 | Example: "https://.execute-api..amazonaws.com/EkeStage/copyProtection"
17 | """
18 | return request.config.getoption("--speke-url")
19 |
20 | @pytest.fixture(scope="session", autouse=True)
21 | def test_suite_dir(request):
22 | if request.config.getoption("--test-vod"):
23 | return "vod"
24 |
25 | return "general"
26 |
27 | @pytest.hookimpl(tryfirst=True)
28 | def pytest_configure(config):
29 | # Create a report folder and configure report name
30 | configure_report_options(config)
31 |
32 | # Create artifacts used in the test suite
33 | if config.getoption("--skip-artifact-generation"):
34 | print("Skipping test artifact generation")
35 | elif config.getoption("--test-vod"):
36 | TestFileGenerator().generate_artifacts(is_vod_suite=True)
37 | else:
38 | TestFileGenerator().generate_artifacts()
39 |
40 | def configure_report_options(config):
41 | date_now = datetime.datetime.now().strftime("%d-%m-%Y %H-%M-%S").replace(" ", "_")
42 | if not os.path.exists('reports'):
43 | os.makedirs('reports')
44 | config.option.htmlpath = 'reports/test_results_' + date_now + ".html"
45 |
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/helpers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/speke-reference-server/5dc9c382532e5c3786ebae3fe540d5ff6ecb6de5/spekev2_verification_testsuite/helpers/__init__.py
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/helpers/utils.py:
--------------------------------------------------------------------------------
1 | import re
2 | import base64
3 | from urllib.parse import urlparse
4 | import xml.etree.ElementTree as ET
5 | from io import StringIO
6 | from collections import Counter
7 | import m3u8
8 | import requests
9 |
10 | from aws_requests_auth.aws_auth import AWSRequestsAuth
11 | from aws_requests_auth import boto_utils
12 |
13 | # FILES USED FOR TESTS
14 | # SpekeV2 test requests for Preset Video 1 and Preset Audio 1 with no Key rotation
15 | GENERIC_WIDEVINE_TEST_FILE = "1_generic_spekev2_dash_widevine_preset_video_1_audio_1_no_rotation.xml"
16 | SPEKEV1_STYLE_REQUEST_WITH_SPEKEV2_HEADERS = "2_speke_v1_style_implementation.xml"
17 | WRONG_VERSION_TEST_FILE = "3_negative_wrong_version_spekev2_dash_widevine.xml" # Wrong CPIX version in request
18 | NEGATIVE_PRESET_SHARED_VIDEO = "4_spekev2_negative_preset_shared_video.xml"
19 | NEGATIVE_PRESET_SHARED_AUDIO = "5_spekev2_negative_preset_shared_audio.xml"
20 |
21 | # TEST CASES
22 | TEST_CASE_1_P_V_1_A_1 = "test_case_1_p_v_1_a_1"
23 | TEST_CASE_2_P_V_3_A_2 = "test_case_2_p_v_3_a_2"
24 | TEST_CASE_3_P_V_5_A_3 = "test_case_3_p_v_5_a_3"
25 | TEST_CASE_4_P_V_8_A_2 = "test_case_4_p_v_8_a_2"
26 | TEST_CASE_5_P_V_2_A_UNENC = "test_case_5_p_v_2_a_unencrypted"
27 | TEST_CASE_6_P_V_UNENC_A_2 = "test_case_6_p_v_unencrypted_a_2"
28 |
29 | # PRESET TEST CASES FILE NAMES
30 | PRESETS_WIDEVINE = "1_widevine.xml"
31 | PRESETS_PLAYREADY = "2_playready.xml"
32 | PRESETS_FAIRPLAY = "3_fairplay.xml"
33 | PRESETS_WIDEVINE_PLAYREADY = "4_widevine_playready.xml"
34 | PRESETS_WIDEVINE_FAIRPLAY = "5_widevine_fairplay.xml"
35 | PRESETS_PLAYREADY_FAIRPLAY = "6_playready_fairplay.xml"
36 | PRESETS_WIDEVINE_PLAYREADY_FAIRPLAY = "7_widevine_playready_fairplay.xml"
37 |
38 | SPEKE_V2_REQUEST_HEADERS = {"x-speke-version": "2.0", 'Content-type': 'application/xml'}
39 | SPEKE_V2_MANDATORY_NAMESPACES = {
40 | "cpix": "urn:dashif:org:cpix",
41 | "pskc": "urn:ietf:params:xml:ns:keyprov:pskc"
42 | }
43 |
44 | SPEKE_V2_CONTENTKEY_COMMONENCRYPTIONSCHEME_ALLOWED_VALUES = ["cenc", "cbc1", "cens", "cbcs"]
45 |
46 | SPEKE_V2_SUPPORTED_INTENDED_TRACK_TYPES = ['VIDEO', 'AUDIO']
47 | SPEKE_V2_SUPPORTED_INTENDED_TRACK_TYPES_VIDEO = [
48 | "VIDEO",
49 | "SD",
50 | "HD",
51 | "UHD",
52 | "SD+HD1",
53 | "HD1",
54 | "HD2",
55 | "UHD1",
56 | "UHD2"
57 | ]
58 | SPEKE_V2_SUPPORTED_INTENDED_TRACK_TYPES_AUDIO = [
59 | "AUDIO",
60 | "STEREO_AUDIO",
61 | "MULTICHANNEL_AUDIO",
62 | "MULTICHANNEL_AUDIO_3_6",
63 | "MULTICHANNEL_AUDIO_7"
64 | ]
65 |
66 | SPEKE_V2_MANDATORY_ELEMENTS_LIST = [
67 | './{urn:dashif:org:cpix}ContentKeyList',
68 | './{urn:dashif:org:cpix}DRMSystemList',
69 | './{urn:dashif:org:cpix}ContentKeyUsageRuleList',
70 | './{urn:dashif:org:cpix}ContentKey',
71 | './{urn:dashif:org:cpix}DRMSystem',
72 | './{urn:dashif:org:cpix}ContentKeyUsageRule'
73 | ]
74 |
75 | SPEKE_V2_MANDATORY_FILTER_ELEMENTS_LIST = [
76 | './{urn:dashif:org:cpix}VideoFilter',
77 | './{urn:dashif:org:cpix}AudioFilter'
78 | ]
79 |
80 | SPEKE_V2_MANDATORY_ATTRIBUTES_LIST = [
81 | ['./{urn:dashif:org:cpix}ContentKey', ['kid', 'commonEncryptionScheme']],
82 | ['./{urn:dashif:org:cpix}DRMSystem', ['kid', 'systemId']],
83 | ['./{urn:dashif:org:cpix}ContentKeyUsageRule', ['kid', 'intendedTrackType']],
84 | ]
85 |
86 | SPEKE_V2_GENERIC_RESPONSE_ELEMENT_LIST = [
87 | '{urn:ietf:params:xml:ns:keyprov:pskc}PlainValue',
88 | '{urn:ietf:params:xml:ns:keyprov:pskc}Secret',
89 | '{urn:dashif:org:cpix}Data',
90 | '{urn:dashif:org:cpix}ContentKey',
91 | '{urn:dashif:org:cpix}ContentKeyList',
92 | '{urn:dashif:org:cpix}PSSH',
93 | '{urn:dashif:org:cpix}DRMSystem',
94 | '{urn:dashif:org:cpix}DRMSystemList',
95 | '{urn:dashif:org:cpix}VideoFilter',
96 | '{urn:dashif:org:cpix}ContentKeyUsageRule',
97 | '{urn:dashif:org:cpix}AudioFilter',
98 | '{urn:dashif:org:cpix}ContentKeyUsageRuleList',
99 | '{urn:dashif:org:cpix}CPIX'
100 | ]
101 |
102 | SPEKE_V2_GENERIC_RESPONSE_ATTRIBS_DICT = {
103 | 'CPIX': ['contentId', 'version'],
104 | 'ContentKey': ['kid', 'commonEncryptionScheme'],
105 | 'DRMSystem': ['kid', 'systemId'],
106 | 'ContentKeyUsageRule': ['kid', 'intendedTrackType']
107 | }
108 |
109 | SPEKE_V2_HLS_SIGNALING_DATA_PLAYLIST_MANDATORY_ATTRIBS = ['media', 'master']
110 |
111 | ## DRM SYSTEM ID LIST
112 | WIDEVINE_SYSTEM_ID = 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'
113 | PLAYREADY_SYSTEM_ID = '9a04f079-9840-4286-ab92-e65be0885f95'
114 | FAIRPLAY_SYSTEM_ID = '94ce86fb-07ff-4f43-adb8-93d2fa968ca2'
115 |
116 | HLS_SIGNALING_DATA_KEYFORMAT = {
117 | 'fairplay': 'com.apple.streamingkeydelivery',
118 | 'playready': 'com.microsoft.playready'
119 | }
120 |
121 |
122 | def read_xml_file_contents(test_type, filename):
123 | with open(f"./spekev2_requests/{test_type}/{filename.strip()}", "r") as f:
124 | return f.read().encode('utf-8')
125 |
126 |
127 | def speke_v2_request(speke_url, request_data):
128 | return requests.post(
129 | url=speke_url,
130 | auth=get_aws_auth(speke_url),
131 | data=request_data,
132 | headers=SPEKE_V2_REQUEST_HEADERS
133 | )
134 |
135 |
136 | def get_aws_auth(url):
137 | api_gateway_netloc = urlparse(url).netloc
138 | api_gateway_region = re.match(
139 | r"[a-z0-9]+\.execute-api\.(.+)\.amazonaws\.com",
140 | api_gateway_netloc
141 | ).group(1)
142 |
143 | return AWSRequestsAuth(
144 | aws_host=api_gateway_netloc,
145 | aws_region=api_gateway_region,
146 | aws_service='execute-api',
147 | **boto_utils.get_credentials()
148 | )
149 |
150 |
151 | def send_speke_request(test_xml_folder, test_xml_file, spekev2_url):
152 | test_request_data = read_xml_file_contents(test_xml_folder, test_xml_file)
153 | response = speke_v2_request(spekev2_url, test_request_data)
154 | return response.text
155 |
156 |
157 | def remove_element(xml_request, element_to_remove, kid_value = ""):
158 | for node in xml_request.iter():
159 | if not kid_value:
160 | for child in node.findall(element_to_remove):
161 | node.remove(child)
162 | else:
163 | for child in node.findall(element_to_remove):
164 | if child.attrib.get("kid") == kid_value:
165 | node.remove(child)
166 |
167 | return xml_request
168 |
169 |
170 | def send_modified_speke_request_with_element_removed(spekev2_url, xml_request_str, element_to_remove):
171 | request_cpix = ET.fromstring(xml_request_str)
172 | modified_cpix_request = remove_element(request_cpix, element_to_remove)
173 | modified_cpix_request_str = ET.tostring(modified_cpix_request, method="xml")
174 | response = speke_v2_request(spekev2_url, modified_cpix_request_str)
175 | return response
176 |
177 |
178 | def send_modified_speke_request_with_matching_elements_kid_values_removed(spekev2_url, xml_request_str, elements_to_remove, kid_values):
179 | request_cpix = ET.fromstring(xml_request_str)
180 |
181 | for elem in elements_to_remove:
182 | for kid in kid_values:
183 | remove_element(request_cpix, elem, kid)
184 |
185 | modified_cpix_request_str = ET.tostring(request_cpix, method="xml")
186 |
187 | response = speke_v2_request(spekev2_url, modified_cpix_request_str)
188 | return response
189 |
190 |
191 | def count_tags(xml_content):
192 | xml_tags = []
193 | for element in ET.iterparse(StringIO(xml_content)):
194 | if type(element) is tuple:
195 | pos, ele = element
196 | xml_tags.append(ele.tag)
197 | else:
198 | xml_tags.append(element.tag)
199 | xml_keys = Counter(xml_tags).keys()
200 | xml_values = Counter(xml_tags).values()
201 | xml_dict = dict(zip(xml_keys, xml_values))
202 | return xml_dict
203 |
204 |
205 | def count_child_element_tags_for_element(parent_element):
206 | xml_tags = [element.tag for element in parent_element]
207 | xml_keys = Counter(xml_tags).keys()
208 | xml_values = Counter(xml_tags).values()
209 | xml_dict = dict(zip(xml_keys, xml_values))
210 | return xml_dict
211 |
212 |
213 | def count_child_element_tags_in_parent(root_cpix, parent_element, child_element):
214 | parent_element_xml = root_cpix.find(parent_element)
215 | return len(parent_element_xml.findall(child_element))
216 |
217 |
218 | def parse_ext_x_key_contents(text_in_bytes):
219 | decoded_text = decode_b64_bytes(text_in_bytes)
220 | return m3u8.loads(decoded_text)
221 |
222 |
223 | def parse_ext_x_session_key_contents(text_in_bytes):
224 | decoded_text = decode_b64_bytes(text_in_bytes).replace("#EXT-X-SESSION-KEY:METHOD", "#EXT-X-KEY:METHOD")
225 | return m3u8.loads(decoded_text)
226 |
227 |
228 | def decode_b64_bytes(text_in_bytes):
229 | return base64.b64decode(text_in_bytes).decode('utf-8')
230 |
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | ;addopts = --html=report.html --self-contained-html
3 | addopts = -vv -rw --html=./results/report.html --self-contained-html
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/spekev2_requests/general/1_generic_spekev2_dash_widevine_preset_video_1_audio_1_no_rotation.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/spekev2_requests/general/2_speke_v1_style_implementation.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/spekev2_requests/general/3_negative_wrong_version_spekev2_dash_widevine.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/spekev2_requests/general/4_spekev2_negative_preset_shared_video.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/spekev2_requests/general/5_spekev2_negative_preset_shared_audio.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/spekev2_requests/vod/1_generic_spekev2_dash_widevine_preset_video_1_audio_1_no_rotation.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/spekev2_requests/vod/2_speke_v1_style_implementation.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/spekev2_requests/vod/3_negative_wrong_version_spekev2_dash_widevine.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/spekev2_requests/vod/4_spekev2_negative_preset_shared_video.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/spekev2_requests/vod/5_spekev2_negative_preset_shared_audio.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/test_basic_checks.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import xml.etree.ElementTree as ET
3 | import time
4 | import re
5 | from .helpers import utils, speke_element_assertions
6 |
7 |
8 | @pytest.fixture(scope="session")
9 | def basic_response(spekev2_url, test_suite_dir):
10 | test_request_data = utils.read_xml_file_contents(test_suite_dir, utils.GENERIC_WIDEVINE_TEST_FILE)
11 | response = utils.speke_v2_request(spekev2_url, test_request_data)
12 | return response
13 |
14 |
15 | @pytest.fixture(scope="session")
16 | def duplicate_responses(spekev2_url, test_suite_dir, request_count=2):
17 | responses = []
18 | for request in range(0, request_count):
19 | test_request_data = utils.read_xml_file_contents(test_suite_dir, utils.GENERIC_WIDEVINE_TEST_FILE)
20 | responses.append(utils.speke_v2_request(spekev2_url, test_request_data).text)
21 | time.sleep(5)
22 | return responses[0] if request_count == 1 else responses
23 |
24 |
25 | @pytest.fixture(scope="session")
26 | def spekev1_style_request(spekev2_url, test_suite_dir):
27 | test_request_data = utils.read_xml_file_contents(test_suite_dir, utils.SPEKEV1_STYLE_REQUEST_WITH_SPEKEV2_HEADERS)
28 | response = utils.speke_v2_request(spekev2_url, test_request_data)
29 | return response
30 |
31 |
32 | def test_status_code(basic_response):
33 | assert basic_response.status_code == 200
34 |
35 |
36 | def test_speke_v2_headers(basic_response):
37 | content_type = basic_response.headers.get('Content-Type').lower()
38 | assert 'application/xml' in content_type, \
39 | "Content-Type must contain application/xml"
40 |
41 | if 'charset' in content_type:
42 | assert 'charset=utf-8' in content_type, \
43 | "Charset value, if present, must be 'utf-8'"
44 |
45 | speke_element_assertions.validate_spekev2_response_headers(basic_response)
46 |
47 |
48 | def test_speke_v2_elements_have_not_changed(basic_response):
49 | # Validate no new elements were added
50 | elements_in_response = list(utils.count_tags(basic_response.text).keys())
51 | assert all(element in elements_in_response for element in utils.SPEKE_V2_GENERIC_RESPONSE_ELEMENT_LIST), \
52 | "Response must not remove any elements present in the request"
53 |
54 | # Validate no new attribs added apart other than the ones in the request
55 | root_cpix = ET.fromstring(basic_response.text)
56 | assert all(attribute in root_cpix.attrib for attribute in utils.SPEKE_V2_GENERIC_RESPONSE_ATTRIBS_DICT['CPIX'])
57 |
58 | # Validate CPIX version is 2.3
59 | speke_element_assertions.check_cpix_version(root_cpix)
60 |
61 | content_key_list_element = root_cpix.find('./{urn:dashif:org:cpix}ContentKeyList')
62 |
63 | content_key_elements = content_key_list_element.findall('{urn:dashif:org:cpix}ContentKey')
64 | for content_key_element in content_key_elements:
65 | assert all(attribute in content_key_element.attrib for attribute in
66 | utils.SPEKE_V2_GENERIC_RESPONSE_ATTRIBS_DICT['ContentKey']), \
67 | "Response must contain values for all mandatory attributes for ContentKey element"
68 | assert content_key_element.get('commonEncryptionScheme') == 'cenc'
69 |
70 | drm_system_list_element = root_cpix.find('./{urn:dashif:org:cpix}DRMSystemList')
71 | drm_system_elements = drm_system_list_element.findall('./{urn:dashif:org:cpix}DRMSystem')
72 | for drm_system_element in drm_system_elements:
73 | assert all(attribute in drm_system_element.attrib for attribute in
74 | utils.SPEKE_V2_GENERIC_RESPONSE_ATTRIBS_DICT['DRMSystem']), \
75 | "Response must contain values for all mandatory attributes for DRMSystem element"
76 | assert drm_system_element.get('systemId') == utils.WIDEVINE_SYSTEM_ID, \
77 | "Request had Widevine SystemID which must remain unchanged"
78 |
79 | content_key_usage_rule_list_element = root_cpix.find('./{urn:dashif:org:cpix}ContentKeyUsageRuleList')
80 | content_key_usage_rule_elements = content_key_usage_rule_list_element.findall(
81 | './{urn:dashif:org:cpix}ContentKeyUsageRule')
82 | for content_key_usage_rule_element in content_key_usage_rule_elements:
83 | assert all(attribute in content_key_usage_rule_element.attrib for attribute in
84 | utils.SPEKE_V2_GENERIC_RESPONSE_ATTRIBS_DICT['ContentKeyUsageRule']), \
85 | "Response must contain values for all mandatory attributes for ContentKeyUsageRule element"
86 | assert content_key_usage_rule_element.get('intendedTrackType') in utils.SPEKE_V2_SUPPORTED_INTENDED_TRACK_TYPES, \
87 | f"intendedTrackType value must be one of {utils.SPEKE_V2_SUPPORTED_INTENDED_TRACK_TYPES}"
88 |
89 |
90 | def test_sending_same_request_sent_twice_to_keyserver_without_key_rotation(duplicate_responses):
91 | test_responses = [response.replace("\n", "").replace("\r", "") for response in duplicate_responses]
92 | regex_pattern = "<(.*):PlainValue>(.*)(.*):PlainValue>(.*)<(.*):PlainValue>(.*)(.*):PlainValue>"
93 | results = []
94 | for response in test_responses:
95 | results.append(re.search(regex_pattern, response))
96 |
97 | if results[0] is not None:
98 | assert results[0].group(2) == results[1].group(2) and results[0].group(6) == results[1].group(6), \
99 | "Keys returned for duplicate responses must be the same with key rotation turned off"
100 | else:
101 | assert False, \
102 | "Pattern matching failed"
103 |
104 |
105 | # Check if this is to be included or we invalidate this request as incorrect in the API
106 | def test_speke_v1_style_request_with_proper_response_received(spekev1_style_request):
107 | speke_element_assertions.validate_spekev2_response_headers(spekev1_style_request)
108 | root_cpix = ET.fromstring(spekev1_style_request.text)
109 |
110 | speke_element_assertions.check_cpix_version(root_cpix)
111 | speke_element_assertions.validate_root_element(root_cpix)
112 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
113 | speke_element_assertions.validate_content_key_list_element(root_cpix, 1, "cenc")
114 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 1, 1, 1, 0, 0)
115 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 1)
116 |
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/test_case_1_preset_video_1_preset_audio_1.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from .helpers import utils, speke_element_assertions
3 | import xml.etree.ElementTree as ET
4 |
5 |
6 | @pytest.fixture(scope="session")
7 | def widevine_response(spekev2_url):
8 | return utils.send_speke_request(utils.TEST_CASE_1_P_V_1_A_1, utils.PRESETS_WIDEVINE, spekev2_url)
9 |
10 |
11 | @pytest.fixture(scope="session")
12 | def playready_response(spekev2_url):
13 | return utils.send_speke_request(utils.TEST_CASE_1_P_V_1_A_1, utils.PRESETS_PLAYREADY, spekev2_url)
14 |
15 |
16 | @pytest.fixture(scope="session")
17 | def fairplay_response(spekev2_url):
18 | return utils.send_speke_request(utils.TEST_CASE_1_P_V_1_A_1, utils.PRESETS_FAIRPLAY, spekev2_url)
19 |
20 |
21 | @pytest.fixture(scope="session")
22 | def widevine_playready_response(spekev2_url):
23 | return utils.send_speke_request(utils.TEST_CASE_1_P_V_1_A_1, utils.PRESETS_WIDEVINE_PLAYREADY, spekev2_url)
24 |
25 |
26 | @pytest.fixture(scope="session")
27 | def widevine_fairplay_response(spekev2_url):
28 | return utils.send_speke_request(utils.TEST_CASE_1_P_V_1_A_1, utils.PRESETS_WIDEVINE_FAIRPLAY, spekev2_url)
29 |
30 |
31 | @pytest.fixture(scope="session")
32 | def playready_fairplay_response(spekev2_url):
33 | return utils.send_speke_request(utils.TEST_CASE_1_P_V_1_A_1, utils.PRESETS_PLAYREADY_FAIRPLAY, spekev2_url)
34 |
35 |
36 | @pytest.fixture(scope="session")
37 | def widevine_playready_fairplay_response(spekev2_url):
38 | return utils.send_speke_request(utils.TEST_CASE_1_P_V_1_A_1, utils.PRESETS_WIDEVINE_PLAYREADY_FAIRPLAY, spekev2_url)
39 |
40 |
41 | def test_case_1_widevine(widevine_response):
42 | root_cpix = ET.fromstring(widevine_response)
43 |
44 | speke_element_assertions.check_cpix_version(root_cpix)
45 | speke_element_assertions.validate_root_element(root_cpix)
46 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
47 | speke_element_assertions.validate_content_key_list_element(root_cpix, 2, "cenc")
48 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 2, 2, 2, 0, 0)
49 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 2)
50 |
51 |
52 | def test_case_1_playready(playready_response):
53 | root_cpix = ET.fromstring(playready_response)
54 |
55 | speke_element_assertions.check_cpix_version(root_cpix)
56 | speke_element_assertions.validate_root_element(root_cpix)
57 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
58 | speke_element_assertions.validate_content_key_list_element(root_cpix, 2, "cenc")
59 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 2, 2, 0, 2, 0)
60 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 2)
61 |
62 |
63 | def test_case_1_fairplay(fairplay_response):
64 | root_cpix = ET.fromstring(fairplay_response)
65 |
66 | speke_element_assertions.check_cpix_version(root_cpix)
67 | speke_element_assertions.validate_root_element(root_cpix)
68 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
69 | speke_element_assertions.validate_content_key_list_element(root_cpix, 2, "cbcs")
70 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 2, 2, 0, 0, 2)
71 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 2)
72 |
73 |
74 | def test_case_1_widevine_playready(widevine_playready_response):
75 | root_cpix = ET.fromstring(widevine_playready_response)
76 |
77 | speke_element_assertions.check_cpix_version(root_cpix)
78 | speke_element_assertions.validate_root_element(root_cpix)
79 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
80 | speke_element_assertions.validate_content_key_list_element(root_cpix, 2, "cenc")
81 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 4, 2, 2, 2, 0)
82 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 2)
83 |
84 |
85 | def test_case_1_widevine_fairplay(widevine_fairplay_response):
86 | root_cpix = ET.fromstring(widevine_fairplay_response)
87 |
88 | speke_element_assertions.check_cpix_version(root_cpix)
89 | speke_element_assertions.validate_root_element(root_cpix)
90 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
91 | speke_element_assertions.validate_content_key_list_element(root_cpix, 2, "cbcs")
92 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 4, 2, 2, 0, 2)
93 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 2)
94 |
95 |
96 | def test_case_1_playready_fairplay(playready_fairplay_response):
97 | root_cpix = ET.fromstring(playready_fairplay_response)
98 |
99 | speke_element_assertions.check_cpix_version(root_cpix)
100 | speke_element_assertions.validate_root_element(root_cpix)
101 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
102 | speke_element_assertions.validate_content_key_list_element(root_cpix, 2, "cbcs")
103 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 4, 2, 0, 2, 2)
104 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 2)
105 |
106 |
107 | def test_case_1_widevine_playready_fairplay(widevine_playready_fairplay_response):
108 | root_cpix = ET.fromstring(widevine_playready_fairplay_response)
109 |
110 | speke_element_assertions.check_cpix_version(root_cpix)
111 | speke_element_assertions.validate_root_element(root_cpix)
112 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
113 | speke_element_assertions.validate_content_key_list_element(root_cpix, 2, "cbcs")
114 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 6, 2, 2, 2, 2)
115 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 2)
116 |
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/test_case_2_preset_video_3_preset_audio_2.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from .helpers import utils, speke_element_assertions
3 | import xml.etree.ElementTree as ET
4 |
5 |
6 | @pytest.fixture(scope="session")
7 | def widevine_response(spekev2_url):
8 | return utils.send_speke_request(utils.TEST_CASE_2_P_V_3_A_2, utils.PRESETS_WIDEVINE, spekev2_url)
9 |
10 |
11 | @pytest.fixture(scope="session")
12 | def playready_response(spekev2_url):
13 | return utils.send_speke_request(utils.TEST_CASE_2_P_V_3_A_2, utils.PRESETS_PLAYREADY, spekev2_url)
14 |
15 |
16 | @pytest.fixture(scope="session")
17 | def fairplay_response(spekev2_url):
18 | return utils.send_speke_request(utils.TEST_CASE_2_P_V_3_A_2, utils.PRESETS_FAIRPLAY, spekev2_url)
19 |
20 |
21 | @pytest.fixture(scope="session")
22 | def widevine_playready_response(spekev2_url):
23 | return utils.send_speke_request(utils.TEST_CASE_2_P_V_3_A_2, utils.PRESETS_WIDEVINE_PLAYREADY, spekev2_url)
24 |
25 |
26 | @pytest.fixture(scope="session")
27 | def widevine_fairplay_response(spekev2_url):
28 | return utils.send_speke_request(utils.TEST_CASE_2_P_V_3_A_2, utils.PRESETS_WIDEVINE_FAIRPLAY, spekev2_url)
29 |
30 |
31 | @pytest.fixture(scope="session")
32 | def playready_fairplay_response(spekev2_url):
33 | return utils.send_speke_request(utils.TEST_CASE_2_P_V_3_A_2, utils.PRESETS_PLAYREADY_FAIRPLAY, spekev2_url)
34 |
35 |
36 | @pytest.fixture(scope="session")
37 | def widevine_playready_fairplay_response(spekev2_url):
38 | return utils.send_speke_request(utils.TEST_CASE_2_P_V_3_A_2, utils.PRESETS_WIDEVINE_PLAYREADY_FAIRPLAY, spekev2_url)
39 |
40 |
41 | def test_case_2_widevine(widevine_response):
42 | root_cpix = ET.fromstring(widevine_response)
43 |
44 | speke_element_assertions.check_cpix_version(root_cpix)
45 | speke_element_assertions.validate_root_element(root_cpix)
46 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
47 | speke_element_assertions.validate_content_key_list_element(root_cpix, 5, "cenc")
48 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 5, 5, 5, 0, 0)
49 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 5)
50 |
51 |
52 | def test_case_2_playready(playready_response):
53 | root_cpix = ET.fromstring(playready_response)
54 |
55 | speke_element_assertions.check_cpix_version(root_cpix)
56 | speke_element_assertions.validate_root_element(root_cpix)
57 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
58 | speke_element_assertions.validate_content_key_list_element(root_cpix, 5, "cenc")
59 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 5, 5, 0, 5, 0)
60 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 5)
61 |
62 |
63 | def test_case_2_fairplay(fairplay_response):
64 | root_cpix = ET.fromstring(fairplay_response)
65 |
66 | speke_element_assertions.check_cpix_version(root_cpix)
67 | speke_element_assertions.validate_root_element(root_cpix)
68 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
69 | speke_element_assertions.validate_content_key_list_element(root_cpix, 5, "cbcs")
70 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 5, 5, 0, 0, 5)
71 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 5)
72 |
73 |
74 | def test_case_2_widevine_playready(widevine_playready_response):
75 | root_cpix = ET.fromstring(widevine_playready_response)
76 |
77 | speke_element_assertions.check_cpix_version(root_cpix)
78 | speke_element_assertions.validate_root_element(root_cpix)
79 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
80 | speke_element_assertions.validate_content_key_list_element(root_cpix, 5, "cenc")
81 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 10, 5, 5, 5, 0)
82 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 5)
83 |
84 |
85 | def test_case_2_widevine_fairplay(widevine_fairplay_response):
86 | root_cpix = ET.fromstring(widevine_fairplay_response)
87 |
88 | speke_element_assertions.check_cpix_version(root_cpix)
89 | speke_element_assertions.validate_root_element(root_cpix)
90 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
91 | speke_element_assertions.validate_content_key_list_element(root_cpix, 5, "cbcs")
92 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 10, 5, 5, 0, 5)
93 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 5)
94 |
95 |
96 | def test_case_2_playready_fairplay(playready_fairplay_response):
97 | root_cpix = ET.fromstring(playready_fairplay_response)
98 |
99 | speke_element_assertions.check_cpix_version(root_cpix)
100 | speke_element_assertions.validate_root_element(root_cpix)
101 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
102 | speke_element_assertions.validate_content_key_list_element(root_cpix, 5, "cbcs")
103 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 10, 5, 0, 5, 5)
104 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 5)
105 |
106 |
107 | def test_case_2_widevine_playready_fairplay(widevine_playready_fairplay_response):
108 | root_cpix = ET.fromstring(widevine_playready_fairplay_response)
109 |
110 | speke_element_assertions.check_cpix_version(root_cpix)
111 | speke_element_assertions.validate_root_element(root_cpix)
112 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
113 | speke_element_assertions.validate_content_key_list_element(root_cpix, 5, "cbcs")
114 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 15, 5, 5, 5, 5)
115 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 5)
116 |
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/test_case_3_preset_video_5_preset_audio_3.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from .helpers import utils, speke_element_assertions
3 | import xml.etree.ElementTree as ET
4 |
5 |
6 | @pytest.fixture(scope="session")
7 | def widevine_response(spekev2_url):
8 | return utils.send_speke_request(utils.TEST_CASE_3_P_V_5_A_3, utils.PRESETS_WIDEVINE, spekev2_url)
9 |
10 |
11 | @pytest.fixture(scope="session")
12 | def playready_response(spekev2_url):
13 | return utils.send_speke_request(utils.TEST_CASE_3_P_V_5_A_3, utils.PRESETS_PLAYREADY, spekev2_url)
14 |
15 |
16 | @pytest.fixture(scope="session")
17 | def fairplay_response(spekev2_url):
18 | return utils.send_speke_request(utils.TEST_CASE_3_P_V_5_A_3, utils.PRESETS_FAIRPLAY, spekev2_url)
19 |
20 |
21 | @pytest.fixture(scope="session")
22 | def widevine_playready_response(spekev2_url):
23 | return utils.send_speke_request(utils.TEST_CASE_3_P_V_5_A_3, utils.PRESETS_WIDEVINE_PLAYREADY, spekev2_url)
24 |
25 |
26 | @pytest.fixture(scope="session")
27 | def widevine_fairplay_response(spekev2_url):
28 | return utils.send_speke_request(utils.TEST_CASE_3_P_V_5_A_3, utils.PRESETS_WIDEVINE_FAIRPLAY, spekev2_url)
29 |
30 |
31 | @pytest.fixture(scope="session")
32 | def playready_fairplay_response(spekev2_url):
33 | return utils.send_speke_request(utils.TEST_CASE_3_P_V_5_A_3, utils.PRESETS_PLAYREADY_FAIRPLAY, spekev2_url)
34 |
35 |
36 | @pytest.fixture(scope="session")
37 | def widevine_playready_fairplay_response(spekev2_url):
38 | return utils.send_speke_request(utils.TEST_CASE_3_P_V_5_A_3, utils.PRESETS_WIDEVINE_PLAYREADY_FAIRPLAY, spekev2_url)
39 |
40 |
41 | def test_case_3_widevine(widevine_response):
42 | root_cpix = ET.fromstring(widevine_response)
43 |
44 | speke_element_assertions.check_cpix_version(root_cpix)
45 | speke_element_assertions.validate_root_element(root_cpix)
46 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
47 | speke_element_assertions.validate_content_key_list_element(root_cpix, 8, "cenc")
48 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 8, 8, 8, 0, 0)
49 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 8)
50 |
51 |
52 | def test_case_3_playready(playready_response):
53 | root_cpix = ET.fromstring(playready_response)
54 |
55 | speke_element_assertions.check_cpix_version(root_cpix)
56 | speke_element_assertions.validate_root_element(root_cpix)
57 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
58 | speke_element_assertions.validate_content_key_list_element(root_cpix, 8, "cenc")
59 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 8, 8, 0, 8, 0)
60 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 8)
61 |
62 |
63 | def test_case_3_fairplay(fairplay_response):
64 | root_cpix = ET.fromstring(fairplay_response)
65 |
66 | speke_element_assertions.check_cpix_version(root_cpix)
67 | speke_element_assertions.validate_root_element(root_cpix)
68 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
69 | speke_element_assertions.validate_content_key_list_element(root_cpix, 8, "cbcs")
70 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 8, 8, 0, 0, 8)
71 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 8)
72 |
73 |
74 | def test_case_3_widevine_playready(widevine_playready_response):
75 | root_cpix = ET.fromstring(widevine_playready_response)
76 |
77 | speke_element_assertions.check_cpix_version(root_cpix)
78 | speke_element_assertions.validate_root_element(root_cpix)
79 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
80 | speke_element_assertions.validate_content_key_list_element(root_cpix, 8, "cenc")
81 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 16, 8, 8, 8, 0)
82 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 8)
83 |
84 |
85 | def test_case_3_widevine_fairplay(widevine_fairplay_response):
86 | root_cpix = ET.fromstring(widevine_fairplay_response)
87 |
88 | speke_element_assertions.check_cpix_version(root_cpix)
89 | speke_element_assertions.validate_root_element(root_cpix)
90 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
91 | speke_element_assertions.validate_content_key_list_element(root_cpix, 8, "cbcs")
92 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 16, 8, 8, 0, 8)
93 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 8)
94 |
95 |
96 | def test_case_3_playready_fairplay(playready_fairplay_response):
97 | root_cpix = ET.fromstring(playready_fairplay_response)
98 |
99 | speke_element_assertions.check_cpix_version(root_cpix)
100 | speke_element_assertions.validate_root_element(root_cpix)
101 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
102 | speke_element_assertions.validate_content_key_list_element(root_cpix, 8, "cbcs")
103 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 16, 8, 0, 8, 8)
104 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 8)
105 |
106 |
107 | def test_case_3_widevine_playready_fairplay(widevine_playready_fairplay_response):
108 | root_cpix = ET.fromstring(widevine_playready_fairplay_response)
109 |
110 | speke_element_assertions.check_cpix_version(root_cpix)
111 | speke_element_assertions.validate_root_element(root_cpix)
112 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
113 | speke_element_assertions.validate_content_key_list_element(root_cpix, 8, "cbcs")
114 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 24, 8, 8, 8, 8)
115 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 8)
116 |
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/test_case_4_preset_video_8_preset_audio_2.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from .helpers import utils, speke_element_assertions
3 | import xml.etree.ElementTree as ET
4 |
5 |
6 | @pytest.fixture(scope="session")
7 | def widevine_response(spekev2_url):
8 | return utils.send_speke_request(utils.TEST_CASE_4_P_V_8_A_2, utils.PRESETS_WIDEVINE, spekev2_url)
9 |
10 |
11 | @pytest.fixture(scope="session")
12 | def playready_response(spekev2_url):
13 | return utils.send_speke_request(utils.TEST_CASE_4_P_V_8_A_2, utils.PRESETS_PLAYREADY, spekev2_url)
14 |
15 |
16 | @pytest.fixture(scope="session")
17 | def fairplay_response(spekev2_url):
18 | return utils.send_speke_request(utils.TEST_CASE_4_P_V_8_A_2, utils.PRESETS_FAIRPLAY, spekev2_url)
19 |
20 |
21 | @pytest.fixture(scope="session")
22 | def widevine_playready_response(spekev2_url):
23 | return utils.send_speke_request(utils.TEST_CASE_4_P_V_8_A_2, utils.PRESETS_WIDEVINE_PLAYREADY, spekev2_url)
24 |
25 |
26 | @pytest.fixture(scope="session")
27 | def widevine_fairplay_response(spekev2_url):
28 | return utils.send_speke_request(utils.TEST_CASE_4_P_V_8_A_2, utils.PRESETS_WIDEVINE_FAIRPLAY, spekev2_url)
29 |
30 |
31 | @pytest.fixture(scope="session")
32 | def playready_fairplay_response(spekev2_url):
33 | return utils.send_speke_request(utils.TEST_CASE_4_P_V_8_A_2, utils.PRESETS_PLAYREADY_FAIRPLAY, spekev2_url)
34 |
35 |
36 | @pytest.fixture(scope="session")
37 | def widevine_playready_fairplay_response(spekev2_url):
38 | return utils.send_speke_request(utils.TEST_CASE_4_P_V_8_A_2, utils.PRESETS_WIDEVINE_PLAYREADY_FAIRPLAY, spekev2_url)
39 |
40 |
41 | def test_case_4_widevine(widevine_response):
42 | root_cpix = ET.fromstring(widevine_response)
43 |
44 | speke_element_assertions.check_cpix_version(root_cpix)
45 | speke_element_assertions.validate_root_element(root_cpix)
46 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
47 | speke_element_assertions.validate_content_key_list_element(root_cpix, 6, "cenc")
48 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 6, 6, 6, 0, 0)
49 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 6)
50 |
51 |
52 | def test_case_4_playready(playready_response):
53 | root_cpix = ET.fromstring(playready_response)
54 |
55 | speke_element_assertions.check_cpix_version(root_cpix)
56 | speke_element_assertions.validate_root_element(root_cpix)
57 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
58 | speke_element_assertions.validate_content_key_list_element(root_cpix, 6, "cenc")
59 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 6, 6, 0, 6, 0)
60 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 6)
61 |
62 |
63 | def test_case_4_fairplay(fairplay_response):
64 | root_cpix = ET.fromstring(fairplay_response)
65 |
66 | speke_element_assertions.check_cpix_version(root_cpix)
67 | speke_element_assertions.validate_root_element(root_cpix)
68 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
69 | speke_element_assertions.validate_content_key_list_element(root_cpix, 6, "cbcs")
70 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 6, 6, 0, 0, 6)
71 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 6)
72 |
73 |
74 | def test_case_4_widevine_playready(widevine_playready_response):
75 | root_cpix = ET.fromstring(widevine_playready_response)
76 |
77 | speke_element_assertions.check_cpix_version(root_cpix)
78 | speke_element_assertions.validate_root_element(root_cpix)
79 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
80 | speke_element_assertions.validate_content_key_list_element(root_cpix, 6, "cenc")
81 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 12, 6, 6, 6, 0)
82 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 6)
83 |
84 |
85 | def test_case_4_widevine_fairplay(widevine_fairplay_response):
86 | root_cpix = ET.fromstring(widevine_fairplay_response)
87 |
88 | speke_element_assertions.check_cpix_version(root_cpix)
89 | speke_element_assertions.validate_root_element(root_cpix)
90 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
91 | speke_element_assertions.validate_content_key_list_element(root_cpix, 6, "cbcs")
92 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 12, 6, 6, 0, 6)
93 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 6)
94 |
95 |
96 | def test_case_4_playready_fairplay(playready_fairplay_response):
97 | root_cpix = ET.fromstring(playready_fairplay_response)
98 |
99 | speke_element_assertions.check_cpix_version(root_cpix)
100 | speke_element_assertions.validate_root_element(root_cpix)
101 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
102 | speke_element_assertions.validate_content_key_list_element(root_cpix, 6, "cbcs")
103 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 12, 6, 0, 6, 6)
104 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 6)
105 |
106 |
107 | def test_case_4_widevine_playready_fairplay(widevine_playready_fairplay_response):
108 | root_cpix = ET.fromstring(widevine_playready_fairplay_response)
109 |
110 | speke_element_assertions.check_cpix_version(root_cpix)
111 | speke_element_assertions.validate_root_element(root_cpix)
112 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
113 | speke_element_assertions.validate_content_key_list_element(root_cpix, 6, "cbcs")
114 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 18, 6, 6, 6, 6)
115 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 6)
116 |
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/test_case_5_preset_video_2_audio_unencrypted.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from .helpers import utils, speke_element_assertions
3 | import xml.etree.ElementTree as ET
4 |
5 |
6 | @pytest.fixture(scope="session")
7 | def widevine_response(spekev2_url):
8 | return utils.send_speke_request(utils.TEST_CASE_5_P_V_2_A_UNENC, utils.PRESETS_WIDEVINE, spekev2_url)
9 |
10 |
11 | @pytest.fixture(scope="session")
12 | def playready_response(spekev2_url):
13 | return utils.send_speke_request(utils.TEST_CASE_5_P_V_2_A_UNENC, utils.PRESETS_PLAYREADY, spekev2_url)
14 |
15 |
16 | @pytest.fixture(scope="session")
17 | def fairplay_response(spekev2_url):
18 | return utils.send_speke_request(utils.TEST_CASE_5_P_V_2_A_UNENC, utils.PRESETS_FAIRPLAY, spekev2_url)
19 |
20 |
21 | @pytest.fixture(scope="session")
22 | def widevine_playready_response(spekev2_url):
23 | return utils.send_speke_request(utils.TEST_CASE_5_P_V_2_A_UNENC, utils.PRESETS_WIDEVINE_PLAYREADY, spekev2_url)
24 |
25 |
26 | @pytest.fixture(scope="session")
27 | def widevine_fairplay_response(spekev2_url):
28 | return utils.send_speke_request(utils.TEST_CASE_5_P_V_2_A_UNENC, utils.PRESETS_WIDEVINE_FAIRPLAY, spekev2_url)
29 |
30 |
31 | @pytest.fixture(scope="session")
32 | def playready_fairplay_response(spekev2_url):
33 | return utils.send_speke_request(utils.TEST_CASE_5_P_V_2_A_UNENC, utils.PRESETS_PLAYREADY_FAIRPLAY, spekev2_url)
34 |
35 |
36 | @pytest.fixture(scope="session")
37 | def widevine_playready_fairplay_response(spekev2_url):
38 | return utils.send_speke_request(utils.TEST_CASE_5_P_V_2_A_UNENC, utils.PRESETS_WIDEVINE_PLAYREADY_FAIRPLAY, spekev2_url)
39 |
40 |
41 | def test_case_5_widevine(widevine_response):
42 | root_cpix = ET.fromstring(widevine_response)
43 |
44 | speke_element_assertions.check_cpix_version(root_cpix)
45 | speke_element_assertions.validate_root_element(root_cpix)
46 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
47 | speke_element_assertions.validate_content_key_list_element(root_cpix, 2, "cenc")
48 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 2, 2, 2, 0, 0)
49 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 2)
50 | speke_element_assertions.validate_content_key_usage_rule_list_for_unencrypted_presets(root_cpix, "audio")
51 |
52 |
53 | def test_case_5_playready(playready_response):
54 | root_cpix = ET.fromstring(playready_response)
55 |
56 | speke_element_assertions.check_cpix_version(root_cpix)
57 | speke_element_assertions.validate_root_element(root_cpix)
58 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
59 | speke_element_assertions.validate_content_key_list_element(root_cpix, 2, "cenc")
60 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 2, 2, 0, 2, 0)
61 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 2)
62 | speke_element_assertions.validate_content_key_usage_rule_list_for_unencrypted_presets(root_cpix, "audio")
63 |
64 |
65 | def test_case_5_fairplay(fairplay_response):
66 | root_cpix = ET.fromstring(fairplay_response)
67 |
68 | speke_element_assertions.check_cpix_version(root_cpix)
69 | speke_element_assertions.validate_root_element(root_cpix)
70 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
71 | speke_element_assertions.validate_content_key_list_element(root_cpix, 2, "cbcs")
72 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 2, 2, 0, 0, 2)
73 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 2)
74 | speke_element_assertions.validate_content_key_usage_rule_list_for_unencrypted_presets(root_cpix, "audio")
75 |
76 |
77 | def test_case_5_widevine_playready(widevine_playready_response):
78 | root_cpix = ET.fromstring(widevine_playready_response)
79 |
80 | speke_element_assertions.check_cpix_version(root_cpix)
81 | speke_element_assertions.validate_root_element(root_cpix)
82 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
83 | speke_element_assertions.validate_content_key_list_element(root_cpix, 2, "cenc")
84 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 4, 2, 2, 2, 0)
85 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 2)
86 | speke_element_assertions.validate_content_key_usage_rule_list_for_unencrypted_presets(root_cpix, "audio")
87 |
88 |
89 | def test_case_5_widevine_fairplay(widevine_fairplay_response):
90 | root_cpix = ET.fromstring(widevine_fairplay_response)
91 |
92 | speke_element_assertions.check_cpix_version(root_cpix)
93 | speke_element_assertions.validate_root_element(root_cpix)
94 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
95 | speke_element_assertions.validate_content_key_list_element(root_cpix, 2, "cbcs")
96 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 4, 2, 2, 0, 2)
97 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 2)
98 | speke_element_assertions.validate_content_key_usage_rule_list_for_unencrypted_presets(root_cpix, "audio")
99 |
100 |
101 | def test_case_5_playready_fairplay(playready_fairplay_response):
102 | root_cpix = ET.fromstring(playready_fairplay_response)
103 |
104 | speke_element_assertions.check_cpix_version(root_cpix)
105 | speke_element_assertions.validate_root_element(root_cpix)
106 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
107 | speke_element_assertions.validate_content_key_list_element(root_cpix, 2, "cbcs")
108 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 4, 2, 0, 2, 2)
109 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 2)
110 | speke_element_assertions.validate_content_key_usage_rule_list_for_unencrypted_presets(root_cpix, "audio")
111 |
112 |
113 | def test_case_5_widevine_playready_fairplay(widevine_playready_fairplay_response):
114 | root_cpix = ET.fromstring(widevine_playready_fairplay_response)
115 |
116 | speke_element_assertions.check_cpix_version(root_cpix)
117 | speke_element_assertions.validate_root_element(root_cpix)
118 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
119 | speke_element_assertions.validate_content_key_list_element(root_cpix, 2, "cbcs")
120 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 6, 2, 2, 2, 2)
121 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 2)
122 | speke_element_assertions.validate_content_key_usage_rule_list_for_unencrypted_presets(root_cpix, "audio")
123 |
124 |
125 |
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/test_case_6_preset_video_unencrypted_a_2.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from .helpers import utils, speke_element_assertions
3 | import xml.etree.ElementTree as ET
4 |
5 |
6 | @pytest.fixture(scope="session")
7 | def widevine_response(spekev2_url):
8 | return utils.send_speke_request(utils.TEST_CASE_6_P_V_UNENC_A_2, utils.PRESETS_WIDEVINE, spekev2_url)
9 |
10 |
11 | @pytest.fixture(scope="session")
12 | def playready_response(spekev2_url):
13 | return utils.send_speke_request(utils.TEST_CASE_6_P_V_UNENC_A_2, utils.PRESETS_PLAYREADY, spekev2_url)
14 |
15 |
16 | @pytest.fixture(scope="session")
17 | def fairplay_response(spekev2_url):
18 | return utils.send_speke_request(utils.TEST_CASE_6_P_V_UNENC_A_2, utils.PRESETS_FAIRPLAY, spekev2_url)
19 |
20 |
21 | @pytest.fixture(scope="session")
22 | def widevine_playready_response(spekev2_url):
23 | return utils.send_speke_request(utils.TEST_CASE_6_P_V_UNENC_A_2, utils.PRESETS_WIDEVINE_PLAYREADY, spekev2_url)
24 |
25 |
26 | @pytest.fixture(scope="session")
27 | def widevine_fairplay_response(spekev2_url):
28 | return utils.send_speke_request(utils.TEST_CASE_6_P_V_UNENC_A_2, utils.PRESETS_WIDEVINE_FAIRPLAY, spekev2_url)
29 |
30 |
31 | @pytest.fixture(scope="session")
32 | def playready_fairplay_response(spekev2_url):
33 | return utils.send_speke_request(utils.TEST_CASE_6_P_V_UNENC_A_2, utils.PRESETS_PLAYREADY_FAIRPLAY, spekev2_url)
34 |
35 |
36 | @pytest.fixture(scope="session")
37 | def widevine_playready_fairplay_response(spekev2_url):
38 | return utils.send_speke_request(utils.TEST_CASE_6_P_V_UNENC_A_2, utils.PRESETS_WIDEVINE_PLAYREADY_FAIRPLAY, spekev2_url)
39 |
40 |
41 | def test_case_6_widevine(widevine_response):
42 | root_cpix = ET.fromstring(widevine_response)
43 |
44 | speke_element_assertions.check_cpix_version(root_cpix)
45 | speke_element_assertions.validate_root_element(root_cpix)
46 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
47 | speke_element_assertions.validate_content_key_list_element(root_cpix, 2, "cenc")
48 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 2, 2, 2, 0, 0)
49 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 2)
50 | speke_element_assertions.validate_content_key_usage_rule_list_for_unencrypted_presets(root_cpix, "video")
51 |
52 |
53 | def test_case_6_playready(playready_response):
54 | root_cpix = ET.fromstring(playready_response)
55 |
56 | speke_element_assertions.check_cpix_version(root_cpix)
57 | speke_element_assertions.validate_root_element(root_cpix)
58 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
59 | speke_element_assertions.validate_content_key_list_element(root_cpix, 2, "cenc")
60 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 2, 2, 0, 2, 0)
61 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 2)
62 | speke_element_assertions.validate_content_key_usage_rule_list_for_unencrypted_presets(root_cpix, "video")
63 |
64 |
65 | def test_case_6_fairplay(fairplay_response):
66 | root_cpix = ET.fromstring(fairplay_response)
67 |
68 | speke_element_assertions.check_cpix_version(root_cpix)
69 | speke_element_assertions.validate_root_element(root_cpix)
70 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
71 | speke_element_assertions.validate_content_key_list_element(root_cpix, 2, "cbcs")
72 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 2, 2, 0, 0, 2)
73 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 2)
74 | speke_element_assertions.validate_content_key_usage_rule_list_for_unencrypted_presets(root_cpix, "video")
75 |
76 |
77 | def test_case_6_widevine_playready(widevine_playready_response):
78 | root_cpix = ET.fromstring(widevine_playready_response)
79 |
80 | speke_element_assertions.check_cpix_version(root_cpix)
81 | speke_element_assertions.validate_root_element(root_cpix)
82 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
83 | speke_element_assertions.validate_content_key_list_element(root_cpix, 2, "cenc")
84 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 4, 2, 2, 2, 0)
85 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 2)
86 | speke_element_assertions.validate_content_key_usage_rule_list_for_unencrypted_presets(root_cpix, "video")
87 |
88 |
89 | def test_case_6_widevine_fairplay(widevine_fairplay_response):
90 | root_cpix = ET.fromstring(widevine_fairplay_response)
91 |
92 | speke_element_assertions.check_cpix_version(root_cpix)
93 | speke_element_assertions.validate_root_element(root_cpix)
94 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
95 | speke_element_assertions.validate_content_key_list_element(root_cpix, 2, "cbcs")
96 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 4, 2, 2, 0, 2)
97 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 2)
98 | speke_element_assertions.validate_content_key_usage_rule_list_for_unencrypted_presets(root_cpix, "video")
99 |
100 |
101 | def test_case_6_playready_fairplay(playready_fairplay_response):
102 | root_cpix = ET.fromstring(playready_fairplay_response)
103 |
104 | speke_element_assertions.check_cpix_version(root_cpix)
105 | speke_element_assertions.validate_root_element(root_cpix)
106 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
107 | speke_element_assertions.validate_content_key_list_element(root_cpix, 2, "cbcs")
108 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 4, 2, 0, 2, 2)
109 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 2)
110 | speke_element_assertions.validate_content_key_usage_rule_list_for_unencrypted_presets(root_cpix, "video")
111 |
112 |
113 | def test_case_6_widevine_playready_fairplay(widevine_playready_fairplay_response):
114 | root_cpix = ET.fromstring(widevine_playready_fairplay_response)
115 |
116 | speke_element_assertions.check_cpix_version(root_cpix)
117 | speke_element_assertions.validate_root_element(root_cpix)
118 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
119 | speke_element_assertions.validate_content_key_list_element(root_cpix, 2, "cbcs")
120 | speke_element_assertions.validate_drm_system_list_element(root_cpix, 6, 2, 2, 2, 2)
121 | speke_element_assertions.validate_content_key_usage_rule_list_element(root_cpix, 2)
122 | speke_element_assertions.validate_content_key_usage_rule_list_for_unencrypted_presets(root_cpix, "video")
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/test_check_mandatory_elements.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import xml.etree.ElementTree as ET
3 | from io import StringIO
4 |
5 | from .helpers import utils, speke_element_assertions
6 |
7 |
8 | @pytest.fixture(scope="session")
9 | def basic_response(spekev2_url, test_suite_dir):
10 | test_request_data = utils.read_xml_file_contents(test_suite_dir, utils.GENERIC_WIDEVINE_TEST_FILE)
11 | response = utils.speke_v2_request(spekev2_url, test_request_data)
12 | return response.text
13 |
14 |
15 | def test_cpix_root_in_response(basic_response):
16 | required_namespaces = utils.SPEKE_V2_MANDATORY_NAMESPACES.values()
17 | namespaces_in_response = dict([node for _, node in ET.iterparse(StringIO(basic_response), events=['start-ns'])]).values()
18 |
19 | # Validate if required namespaces are present in the response
20 | assert all(ns in namespaces_in_response for ns in required_namespaces), \
21 | f"Requred namespaces must be present in response: {required_namespaces}"
22 |
23 | root_cpix = ET.fromstring(basic_response)
24 |
25 | # Validate if CPIX has required attribute version and its value is 2.3
26 | speke_element_assertions.check_cpix_version(root_cpix)
27 |
28 | # Validate if root element is CPIX and check mandatory attributes
29 | speke_element_assertions.validate_root_element(root_cpix)
30 |
31 | # Validate if CPIX element has all mandatory child elements present and have only 1 instance
32 | speke_element_assertions.validate_mandatory_cpix_child_elements(root_cpix)
33 |
34 |
35 | def test_cpix_content_key_list_in_response(basic_response):
36 | root_cpix = ET.fromstring(basic_response)
37 |
38 | # Validate attributes and elements for content_key_list element
39 | content_key_list_element = root_cpix.find('./{urn:dashif:org:cpix}ContentKeyList')
40 | content_key_list_child_elements = utils.count_child_element_tags_for_element(content_key_list_element)
41 |
42 | # Validate presence of ContentKey element
43 | assert '{urn:dashif:org:cpix}ContentKey' in content_key_list_child_elements, \
44 | "Atleast one ContentKey element is expected in ContentKeyList"
45 | assert content_key_list_child_elements.get('{urn:dashif:org:cpix}ContentKey') >= 1, \
46 | "Atleast one ContentKey element is expected in ContentKeyList"
47 |
48 | # Validate mandatory attributes for ContentKey element
49 | content_key_elements = content_key_list_element.findall('{urn:dashif:org:cpix}ContentKey')
50 |
51 | for content_key_element in content_key_elements:
52 | assert content_key_element.get('kid'), \
53 | "kid is a mandatory attribute for ContentKey element"
54 | assert content_key_element.get('commonEncryptionScheme'), \
55 | "commonEncryptionScheme is a mandatory attribute for ContentKey element"
56 | assert content_key_element.get(
57 | 'commonEncryptionScheme') in utils.SPEKE_V2_CONTENTKEY_COMMONENCRYPTIONSCHEME_ALLOWED_VALUES, \
58 | f"commonEncryptionScheme is a mandatory attribute for ContentKey element and must be one of {utils.SPEKE_V2_CONTENTKEY_COMMONENCRYPTIONSCHEME_ALLOWED_VALUES}"
59 |
60 | # Validate ContentKey has Data element present.
61 | # This element is not in the request but response is expected to have it.
62 | content_key_child_elements = utils.count_child_element_tags_for_element(content_key_element)
63 | assert '{urn:dashif:org:cpix}Data' in content_key_child_elements
64 | assert content_key_child_elements.get('{urn:dashif:org:cpix}Data') == 1, \
65 | "Data is a mandatory child element of ContentKey"
66 |
67 | # Validate Data has Secret element present
68 | data_elements = content_key_element.findall('{urn:dashif:org:cpix}Data')
69 | assert data_elements
70 |
71 | for data_element in data_elements:
72 | data_child_elements = utils.count_child_element_tags_for_element(data_element)
73 | assert '{urn:ietf:params:xml:ns:keyprov:pskc}Secret' in data_child_elements
74 | assert data_child_elements.get('{urn:ietf:params:xml:ns:keyprov:pskc}Secret') == 1, \
75 | "Secret is a mandatory child element of Data"
76 |
77 | # Validate Secret has either PlainValue or EncryptedValue element present
78 | secret_elements = data_element.findall('{urn:ietf:params:xml:ns:keyprov:pskc}Secret')
79 | assert secret_elements
80 |
81 | for secret_element in secret_elements:
82 | assert secret_element.find(
83 | '{urn:ietf:params:xml:ns:keyprov:pskc}PlainValue').text or secret_element.find(
84 | '{urn:ietf:params:xml:ns:keyprov:pskc}EncryptedValue').text, \
85 | "Either PlainValue or EncryptedValue child element is expected within Secret"
86 |
87 |
88 | def test_drm_system_list_in_response(basic_response):
89 | root_cpix = ET.fromstring(basic_response)
90 | drm_system_list_element = root_cpix.find('./{urn:dashif:org:cpix}DRMSystemList')
91 | drm_system_list_child_elements = utils.count_child_element_tags_for_element(drm_system_list_element)
92 |
93 | # Validate presence of DRMSystem in DRMSystemList
94 | assert '{urn:dashif:org:cpix}DRMSystem' in drm_system_list_child_elements
95 | assert drm_system_list_child_elements.get('{urn:dashif:org:cpix}DRMSystem') >= 1, \
96 | "DRMSystem is a mandatory child element of DRMSystemList"
97 |
98 |
99 | def test_content_key_usage_rule_list_in_response(basic_response):
100 | root_cpix = ET.fromstring(basic_response)
101 | content_key_usage_rule_list_element = root_cpix.find('./{urn:dashif:org:cpix}ContentKeyUsageRuleList')
102 | content_key_usage_rule_child_elements = utils.count_child_element_tags_for_element(
103 | content_key_usage_rule_list_element)
104 |
105 | # Validate presence of ContentKeyUsageRule in ContentKeyUsageRuleList
106 | assert content_key_usage_rule_child_elements
107 | assert '{urn:dashif:org:cpix}ContentKeyUsageRule' in content_key_usage_rule_child_elements, \
108 | "ContentKeyUsageRule is a mandatory child element of ContentKeyUsageRuleList"
109 | assert content_key_usage_rule_child_elements.get('{urn:dashif:org:cpix}ContentKeyUsageRule') >= 1, \
110 | "Atleast 1 ContentKeyUsageRule is expected under ContentKeyUsageRuleList"
111 |
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/test_drm_system_specific_system_id_elements.py:
--------------------------------------------------------------------------------
1 | import xml.etree.ElementTree as ET
2 | import pytest
3 | from .helpers import utils
4 |
5 |
6 | @pytest.fixture(scope="session")
7 | def playready_pssh_cpd_response(spekev2_url):
8 | test_request_data = utils.read_xml_file_contents("test_case_1_p_v_1_a_1", utils.PRESETS_PLAYREADY)
9 | response = utils.speke_v2_request(spekev2_url, test_request_data)
10 | return response.text
11 |
12 |
13 | @pytest.fixture(scope="session")
14 | def widevine_pssh_cpd_response(spekev2_url):
15 | test_request_data = utils.read_xml_file_contents("test_case_1_p_v_1_a_1", utils.PRESETS_WIDEVINE)
16 | response = utils.speke_v2_request(spekev2_url, test_request_data)
17 | return response.text
18 |
19 |
20 | @pytest.fixture(scope="session")
21 | def fairplay_hls_signalingdata_response(spekev2_url):
22 | test_request_data = utils.read_xml_file_contents("test_case_1_p_v_1_a_1", utils.PRESETS_FAIRPLAY)
23 | response = utils.speke_v2_request(spekev2_url, test_request_data)
24 | return response.text
25 |
26 |
27 | def test_widevine_pssh_cpd_no_rotation(widevine_pssh_cpd_response):
28 | root_cpix = ET.fromstring(widevine_pssh_cpd_response)
29 | drm_system_list_element = root_cpix.find('./{urn:dashif:org:cpix}DRMSystemList')
30 | drm_system_elements = drm_system_list_element.findall('./{urn:dashif:org:cpix}DRMSystem')
31 |
32 | for drm_system_element in drm_system_elements:
33 | pssh_data_bytes = drm_system_element.find('./{urn:dashif:org:cpix}PSSH')
34 |
35 | content_protection_data_bytes = drm_system_element.find('./{urn:dashif:org:cpix}ContentProtectionData')
36 | content_protection_data_string = utils.decode_b64_bytes(content_protection_data_bytes.text)
37 | pssh_in_cpd = ET.fromstring(content_protection_data_string)
38 |
39 | # Assert pssh in cpd is same as pssh box
40 | assert pssh_data_bytes.text == pssh_in_cpd.text, \
41 | "Content in PSSH box and the requested content in ContentProtectionData are expected to be the same"
42 |
43 |
44 | def test_dash_playready_pssh_cpd_no_rotation(playready_pssh_cpd_response):
45 | root_cpix = ET.fromstring(playready_pssh_cpd_response)
46 | drm_system_list_element = root_cpix.find('./{urn:dashif:org:cpix}DRMSystemList')
47 | drm_system_elements = drm_system_list_element.findall('./{urn:dashif:org:cpix}DRMSystem')
48 |
49 | for drm_system_element in drm_system_elements:
50 | pssh_data_bytes = drm_system_element.find('./{urn:dashif:org:cpix}PSSH')
51 |
52 | content_protection_data_bytes = drm_system_element.find('./{urn:dashif:org:cpix}ContentProtectionData')
53 | content_protection_data_string = utils.decode_b64_bytes(content_protection_data_bytes.text)
54 |
55 | cpd_xml = '' + content_protection_data_string + ''
56 |
57 | cpd_root = ET.fromstring(cpd_xml)
58 | pssh_in_cpd = cpd_root.find("./{urn:mpeg:cenc:2013}pssh")
59 |
60 | # Assert pssh in cpd is same as pssh box
61 | assert pssh_data_bytes.text == pssh_in_cpd.text, \
62 | "Content in PSSH box and the requested content in ContentProtectionData are expected to be the same"
63 |
64 |
65 | # Validate presence of HLSSignalingData and PSSH when those elements are present in the request
66 | def test_playready_pssh_hlssignalingdata_no_rotation(playready_pssh_cpd_response):
67 | root_cpix = ET.fromstring(playready_pssh_cpd_response)
68 | drm_system_list_element = root_cpix.find('./{urn:dashif:org:cpix}DRMSystemList')
69 | drm_system_elements = drm_system_list_element.findall('./{urn:dashif:org:cpix}DRMSystem')
70 |
71 | for drm_system_element in drm_system_elements:
72 | pssh_data_bytes = drm_system_element.find('./{urn:dashif:org:cpix}PSSH')
73 | assert pssh_data_bytes.text, \
74 | "PSSH must not be empty"
75 |
76 | hls_signalling_data_elems = drm_system_element.findall('./{urn:dashif:org:cpix}HLSSignalingData')
77 | # Two elements are expected, one for media and other for master
78 | assert len(hls_signalling_data_elems) == 2, \
79 | "Two HLSSignalingData elements are expected for this request: media and master, received {}".format(
80 | hls_signalling_data_elems)
81 |
82 | # Check if HLSSignalingData text is present in the response
83 | hls_signalling_data_media = "{urn:dashif:org:cpix}HLSSignalingData[@playlist='media']"
84 | assert drm_system_element.find(hls_signalling_data_media).text, \
85 | "One HLSSignalingData element is expected to have a playlist value of media"
86 | hls_signalling_data_master = "{urn:dashif:org:cpix}HLSSignalingData[@playlist='master']"
87 | assert drm_system_element.find(hls_signalling_data_master).text, \
88 | "One HLSSignalingData element is expected to have a playlist value of master"
89 |
90 | received_playlist_atrrib_values = [hls_signalling_data.get('playlist') for hls_signalling_data in
91 | hls_signalling_data_elems]
92 |
93 | # Check both media and master attributes are present in the response
94 | assert all(attribute in received_playlist_atrrib_values for attribute in
95 | utils.SPEKE_V2_HLS_SIGNALING_DATA_PLAYLIST_MANDATORY_ATTRIBS), \
96 | "Two HLSSignalingData elements, with playlist values of media and master are expected"
97 |
98 | str_ext_x_key = utils.parse_ext_x_key_contents(drm_system_element.find(hls_signalling_data_media).text)
99 | # Treat ext-x-session-key as ext-x-key for purposes of this validation
100 | str_ext_x_session_key = utils.parse_ext_x_session_key_contents(
101 | drm_system_element.find(hls_signalling_data_master).text)
102 |
103 | # Assert that str_ext_x_key and str_ext_x_session_key contents are present and parsed correctly
104 | assert str_ext_x_key.keys, \
105 | "EXT-X-KEY was not parsed correctly"
106 | assert str_ext_x_session_key.keys, \
107 | "EXT-X-SESSION-KEY was not parsed correctly"
108 |
109 | # Value of (EXT-X-SESSION-KEY) METHOD attribute MUST NOT be NONE
110 | assert str_ext_x_session_key.keys[0].method, \
111 | "EXT-X-SESSION-KEY METHOD must not be NONE"
112 |
113 | # If an EXT-X-SESSION-KEY is used, the values of the METHOD, KEYFORMAT, and KEYFORMATVERSIONS attributes MUST
114 | # match any EXT-X-KEY with the same URI value
115 | assert str_ext_x_key.keys[0].method == str_ext_x_session_key.keys[0].method, \
116 | "METHOD for #EXT-X-KEY and EXT-X-SESSION-KEY must match for this request"
117 | assert str_ext_x_key.keys[0].keyformat == str_ext_x_session_key.keys[0].keyformat, \
118 | "KEYFORMAT for #EXT-X-KEY and EXT-X-SESSION-KEY must match for this request"
119 | assert str_ext_x_key.keys[0].keyformatversions == str_ext_x_session_key.keys[0].keyformatversions, \
120 | "KEYFORMATVERSIONS for #EXT-X-KEY and EXT-X-SESSION-KEY must match for this request"
121 |
122 | # Relaxing this requirement, this was originally added as we do not currently support different values
123 | # for the two signaling levels.
124 | # assert str_ext_x_key.keys[0].uri == str_ext_x_session_key.keys[0].uri, \
125 | # "URI for #EXT-X-KEY and EXT-X-SESSION-KEY must match for this request"
126 |
127 | assert str_ext_x_key.keys[0].keyformat == str_ext_x_session_key.keys[
128 | 0].keyformat == utils.HLS_SIGNALING_DATA_KEYFORMAT.get("playready"), \
129 | f"KEYFORMAT value is expected to be com.microsoft.playready for playready"
130 |
131 |
132 | def test_fairplay_hlssignalingdata_no_rotation(fairplay_hls_signalingdata_response):
133 | root_cpix = ET.fromstring(fairplay_hls_signalingdata_response)
134 | drm_system_list_element = root_cpix.find('./{urn:dashif:org:cpix}DRMSystemList')
135 | drm_system_elements = drm_system_list_element.findall('./{urn:dashif:org:cpix}DRMSystem')
136 |
137 | for drm_system_element in drm_system_elements:
138 | pssh_data_bytes = drm_system_element.find('./{urn:dashif:org:cpix}PSSH')
139 | assert not pssh_data_bytes, \
140 | "PSSH must not be empty"
141 |
142 | hls_signalling_data_elems = drm_system_element.findall('./{urn:dashif:org:cpix}HLSSignalingData')
143 | # Two elements are expected, one for media and other for master
144 | assert len(hls_signalling_data_elems) == 2, \
145 | "Two HLSSignalingData elements are expected for this request: media and master, received {}".format(
146 | hls_signalling_data_elems)
147 |
148 | # Check if HLSSignalingData text is present in the response
149 | hls_signalling_data_media = "{urn:dashif:org:cpix}HLSSignalingData[@playlist='media']"
150 | assert drm_system_element.find(hls_signalling_data_media).text, \
151 | "One HLSSignalingData element is expected to have a playlist value of media"
152 | hls_signalling_data_master = "{urn:dashif:org:cpix}HLSSignalingData[@playlist='master']"
153 | assert drm_system_element.find(hls_signalling_data_master).text, \
154 | "One HLSSignalingData element is expected to have a playlist value of master"
155 |
156 | received_playlist_atrrib_values = [hls_signalling_data.get('playlist') for hls_signalling_data in
157 | hls_signalling_data_elems]
158 |
159 | # Check both media and master attributes are present in the response
160 | assert all(attribute in received_playlist_atrrib_values for attribute in
161 | utils.SPEKE_V2_HLS_SIGNALING_DATA_PLAYLIST_MANDATORY_ATTRIBS), \
162 | "Two HLSSignalingData elements, with playlist values of media and master are expected"
163 |
164 | str_ext_x_key = utils.parse_ext_x_key_contents(drm_system_element.find(hls_signalling_data_media).text)
165 | # Treat ext-x-session-key as ext-x-key for purposes of this validation
166 | str_ext_x_session_key = utils.parse_ext_x_session_key_contents(
167 | drm_system_element.find(hls_signalling_data_master).text)
168 |
169 | # Assert that str_ext_x_key and str_ext_x_session_key contents are present and parsed correctly
170 | assert str_ext_x_key.keys, \
171 | "EXT-X-KEY was not parsed correctly"
172 | assert str_ext_x_session_key.keys, \
173 | "EXT-X-SESSION-KEY was not parsed correctly"
174 |
175 | # Value of (EXT-X-SESSION-KEY) METHOD attribute MUST NOT be NONE
176 | assert str_ext_x_session_key.keys[0].method, \
177 | "EXT-X-SESSION-KEY METHOD must not be NONE"
178 |
179 | # If an EXT-X-SESSION-KEY is used, the values of the METHOD, KEYFORMAT, and KEYFORMATVERSIONS attributes MUST
180 | # match any EXT-X-KEY with the same URI value
181 | assert str_ext_x_key.keys[0].method == str_ext_x_session_key.keys[0].method, \
182 | "METHOD for #EXT-X-KEY and EXT-X-SESSION-KEY must match for this request"
183 | assert str_ext_x_key.keys[0].keyformat == str_ext_x_session_key.keys[0].keyformat, \
184 | "KEYFORMAT for #EXT-X-KEY and EXT-X-SESSION-KEY must match for this request"
185 | assert str_ext_x_key.keys[0].keyformatversions == str_ext_x_session_key.keys[0].keyformatversions, \
186 | "KEYFORMATVERSIONS for #EXT-X-KEY and EXT-X-SESSION-KEY must match for this request"
187 | assert str_ext_x_key.keys[0].uri == str_ext_x_session_key.keys[0].uri, \
188 | "URI for #EXT-X-KEY and EXT-X-SESSION-KEY must match for this request"
189 |
190 | assert str_ext_x_key.keys[0].keyformat == str_ext_x_session_key.keys[
191 | 0].keyformat == utils.HLS_SIGNALING_DATA_KEYFORMAT.get("fairplay"), \
192 | f"KEYFORMAT value is expected to be com.apple.streamingkeydelivery for Fairplay"
193 |
--------------------------------------------------------------------------------
/spekev2_verification_testsuite/test_negative_cases.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import xml.etree.ElementTree as ET
3 | from io import StringIO
4 | from .helpers import utils, speke_element_assertions
5 |
6 |
7 | @pytest.fixture
8 | def generic_request(spekev2_url, test_suite_dir):
9 | return utils.read_xml_file_contents(test_suite_dir, utils.GENERIC_WIDEVINE_TEST_FILE)
10 |
11 | @pytest.fixture
12 | def fairplay_request(spekev2_url):
13 | return utils.read_xml_file_contents(utils.TEST_CASE_1_P_V_1_A_1, utils.PRESETS_FAIRPLAY)
14 |
15 |
16 | @pytest.fixture
17 | def preset_negative_preset_shared_video(spekev2_url, test_suite_dir):
18 | return utils.read_xml_file_contents(test_suite_dir, utils.NEGATIVE_PRESET_SHARED_VIDEO)
19 |
20 |
21 | @pytest.fixture
22 | def preset_negative_preset_shared_audio(spekev2_url, test_suite_dir):
23 | return utils.read_xml_file_contents(test_suite_dir, utils.NEGATIVE_PRESET_SHARED_AUDIO)
24 |
25 |
26 | @pytest.fixture
27 | def empty_xml_response(spekev2_url):
28 | response = utils.speke_v2_request(spekev2_url, "")
29 | return response
30 |
31 |
32 | @pytest.fixture
33 | def wrong_version_response(spekev2_url, test_suite_dir):
34 | test_request_data = utils.read_xml_file_contents(test_suite_dir, utils.WRONG_VERSION_TEST_FILE)
35 | response = utils.speke_v2_request(spekev2_url, test_request_data)
36 | return response
37 |
38 |
39 | def test_empty_request(empty_xml_response):
40 | assert empty_xml_response.status_code != 200 and (400 <= empty_xml_response.status_code < 600), \
41 | "Empty request is expected to return an error"
42 |
43 |
44 | def test_wrong_version_status_code(wrong_version_response):
45 | assert wrong_version_response.status_code != 200 and (400 <= wrong_version_response.status_code < 600), \
46 | "Wrong version in the request is expected to return an error"
47 |
48 |
49 | @pytest.mark.parametrize("mandatory_element", utils.SPEKE_V2_MANDATORY_ELEMENTS_LIST)
50 | def test_mandatory_elements_missing_in_request(spekev2_url, generic_request, mandatory_element):
51 | response = utils.send_modified_speke_request_with_element_removed(spekev2_url, generic_request, mandatory_element)
52 | assert response.status_code != 200 and (400 <= response.status_code < 600), \
53 | f"Mandatory element: {mandatory_element} not present in request but response was a 200 OK"
54 |
55 |
56 | def test_both_mandatory_filter_elements_missing_in_request(spekev2_url, generic_request):
57 | request_cpix = ET.fromstring(generic_request)
58 | for node in request_cpix.iter():
59 | for elem in utils.SPEKE_V2_MANDATORY_FILTER_ELEMENTS_LIST:
60 | for child in node.findall(elem):
61 | node.remove(child)
62 |
63 | request_xml_data = ET.tostring(request_cpix, method="xml")
64 | response = utils.speke_v2_request(spekev2_url, request_xml_data)
65 |
66 | assert response.status_code != 200 and (400 <= response.status_code < 600), \
67 | f"Mandatory filter elements: {utils.SPEKE_V2_MANDATORY_FILTER_ELEMENTS_LIST} not present in request but " \
68 | f"response was a 200 OK "
69 |
70 |
71 | @pytest.mark.parametrize("mandatory_attribute", utils.SPEKE_V2_MANDATORY_ATTRIBUTES_LIST)
72 | def test_missing_mandatory_attributes_in_request(spekev2_url, generic_request, mandatory_attribute):
73 | request_cpix = ET.fromstring(generic_request)
74 | for node in request_cpix.iter():
75 | for child in node.findall(mandatory_attribute[0]):
76 | for attribute in [x for x in mandatory_attribute[1] if x in child.attrib]:
77 | child.attrib.pop(attribute)
78 |
79 | request_xml_data = ET.tostring(request_cpix, method="xml")
80 | response = utils.speke_v2_request(spekev2_url, request_xml_data)
81 |
82 | assert response.status_code != 200 and (400 <= response.status_code < 600), \
83 | f"Mandatory attribute(s): {mandatory_attribute[1]} for element: {mandatory_attribute[0]} not present in " \
84 | f"request but response was a 200 OK "
85 |
86 |
87 | def test_common_encryption_scheme_for_fairplay_should_not_be_cenc(spekev2_url, fairplay_request):
88 | xml_request = fairplay_request.decode('UTF-8').replace("cbcs", "cenc")
89 | response = utils.speke_v2_request(spekev2_url, xml_request.encode('UTF-8'))
90 |
91 | assert response.status_code != 200 and (400 <= response.status_code < 600), \
92 | f"Requests for Fairplay DRM system ID should not include cenc as common encryption. Status code returned was {response.status_code}"
93 |
94 |
95 | def test_video_preset_2_and_shared_audio_preset_request_expect_4xx(spekev2_url, preset_negative_preset_shared_audio):
96 | """
97 | Intended track type(s) used in this test are SD, ALL, STEREO_AUDIO, MULTICHANNEL_AUDIO
98 | Expected to return HTTP 4xx error
99 | """
100 |
101 | xml_request = preset_negative_preset_shared_audio.decode('UTF-8')
102 | response = utils.speke_v2_request(spekev2_url, xml_request.encode('UTF-8'))
103 |
104 | assert response.status_code != 200 and (400 <= response.status_code < 600), \
105 | f"If intendedTrackType with ALL is requested, there cannot be other ContentKeyUsageRule elements with " \
106 | f"different intendedTrackType values "
107 |
108 |
109 | def test_shared_video_preset_and_audio_preset_2_request_expect_4xx(spekev2_url, preset_negative_preset_shared_video):
110 | """
111 | Intended track type(s) used in this test are SD, HD, ALL, MULTICHANNEL_AUDIO
112 | :returns: HTTP 4xx error
113 | """
114 |
115 | xml_request = preset_negative_preset_shared_video.decode('UTF-8')
116 | response = utils.speke_v2_request(spekev2_url, xml_request.encode('UTF-8'))
117 |
118 | assert response.status_code != 200 and (400 <= response.status_code < 600), \
119 | f"If intendedTrackType with ALL is requested, there cannot be other ContentKeyUsageRule elements with " \
120 | f"different intendedTrackType values "
121 |
--------------------------------------------------------------------------------
/src/key_cache.py:
--------------------------------------------------------------------------------
1 | """
2 | http://www.apache.org/licenses/LICENSE-2.0
3 |
4 | Unless required by applicable law or agreed to in writing,
5 | software distributed under the License is distributed on an
6 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
7 | KIND, either express or implied. See the License for the
8 | specific language governing permissions and limitations
9 | under the License.
10 | """
11 |
12 | import boto3
13 |
14 |
15 | class KeyCache:
16 | """
17 | This class is responsible for storing keys in the key cache (S3) and
18 | returning a URL that can return a specific key from the cache.
19 | """
20 |
21 | def __init__(self, keystore_bucket, client_url_prefix):
22 | self.keystore_bucket = keystore_bucket
23 | self.client_url_prefix = client_url_prefix
24 |
25 | def store(self, content_id, key_id, key_value):
26 | """
27 | Store a key into the cache (S3) using the content_id
28 | as a folder and key_id as the file
29 | """
30 | key = "{cid}/{kid}".format(cid=content_id, kid=key_id)
31 | s3_client = boto3.client('s3')
32 | # store the key file with public-read permissions
33 | # public bucket policy not required
34 | s3_client.put_object(Bucket=self.keystore_bucket, Key=key, Body=key_value)
35 |
36 | def url(self, content_id, key_id):
37 | """
38 | Return a URL that can be used to retrieve the
39 | specified key_id related to content_id
40 | """
41 | return "{}/{}/{}".format(self.client_url_prefix, content_id, key_id)
42 |
--------------------------------------------------------------------------------
/src/key_generator.py:
--------------------------------------------------------------------------------
1 | """
2 | http://www.apache.org/licenses/LICENSE-2.0
3 |
4 | Unless required by applicable law or agreed to in writing,
5 | software distributed under the License is distributed on an
6 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
7 | KIND, either express or implied. See the License for the
8 | specific language governing permissions and limitations
9 | under the License.
10 | """
11 |
12 | import hashlib
13 | # import secrets
14 |
15 | import boto3
16 | from botocore.exceptions import ClientError
17 | from cryptography.hazmat.primitives import hashes
18 | from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
19 | from cryptography.hazmat.backends import default_backend
20 |
21 |
22 | class KeyGenerator:
23 | """
24 | This class is responsible for symmetric key generation. Different
25 | functions are provided to generate keys. This class also manages the
26 | secret data used by each content ID in key generation.
27 | """
28 |
29 | def __init__(self):
30 | self.backend = default_backend()
31 | self.content_id_secret_length = 64
32 | self.derived_key_iterations = 5000
33 | self.derived_key_size = 16
34 | self.keyed_hash_digest_size = 16
35 | self.local_secret_folder = "/tmp"
36 | self.secrets_client = boto3.client('secretsmanager')
37 |
38 | def md5_key(self, secret, kid):
39 | """
40 | Generate a key using an MD5 digest function
41 | """
42 | md5 = hashlib.md5()
43 | md5.update(secret.encode('utf-8'))
44 | md5.update(kid.encode('utf-8'))
45 | return md5.digest()
46 |
47 | def blake2b_key(self, secret, kid):
48 | """
49 | Generate a key using the Blake2B variable-length digest function
50 | """
51 | blake_hash = hashlib.blake2b(digest_size=self.keyed_hash_digest_size)
52 | blake_hash.update(secret.encode('utf-8'))
53 | blake_hash.update(kid.encode('utf-8'))
54 | return blake_hash.digest()
55 |
56 | def derived_key(self, secret, kid):
57 | """
58 | Generate a key using a key derivation function (default)
59 | """
60 | kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=self.derived_key_size, salt=secret.encode('utf-8'), iterations=self.derived_key_iterations, backend=self.backend)
61 | return kdf.derive(kid.encode('utf-8'))
62 |
63 | def local_secret_path(self, content_id):
64 | """
65 | Create a path for a content ID secret file stored locally in the Lambda filesystem
66 | """
67 | return '{}/speke.{}'.format(self.local_secret_folder, content_id)
68 |
69 | def store_local_secret(self, content_id, secret):
70 | """
71 | Store a content ID secret file
72 | """
73 | secret_file = self.local_secret_path(content_id)
74 | secret_file = open(secret_file, 'w')
75 | secret_file.write(secret)
76 | secret_file.close()
77 |
78 | def retrieve_local_secret(self, content_id):
79 | """
80 | Retrieve a content ID secret file
81 | """
82 | secret_file = self.local_secret_path(content_id)
83 | secret_file = open(secret_file, 'r')
84 | secret = secret_file.read()
85 | secret_file.close()
86 | return secret
87 |
88 | def generate_content_id_secret(self):
89 | """
90 | Create a string of random text used in generating a key for a content ID/key ID
91 | """
92 | # return secrets.token_hex(self.content_id_secret_length)
93 | return self.secrets_client.get_random_password(PasswordLength=self.content_id_secret_length)['RandomPassword']
94 |
95 | def retrieve_content_id_secret(self, content_id):
96 | """
97 | Retrieve the secret value by content ID used for generating keys
98 | """
99 | try:
100 | # cached locally?
101 | secret = self.retrieve_local_secret(content_id)
102 | print("CACHED-SECRET {}".format(content_id))
103 | except IOError:
104 | # try secrets manager
105 | secret_id = "speke/{}".format(content_id)
106 | try:
107 | response = self.secrets_client.get_secret_value(SecretId=secret_id)
108 | secret = response['SecretString']
109 | self.store_local_secret(content_id, secret)
110 | print("RETRIEVE-SECRET {}".format(content_id))
111 | except ClientError as error:
112 | if error.response['Error']['Code'] == 'ResourceNotFoundException':
113 | # we need a new secret value
114 | print("CREATE-SECRET {}".format(content_id))
115 | secret = self.generate_content_id_secret()
116 | self.secrets_client.create_secret(Name=secret_id, SecretString=secret, Description='SPEKE content ID secret value for key generation')
117 | self.store_local_secret(content_id, secret)
118 | else:
119 | # we're done trying
120 | raise error
121 | return secret
122 |
123 | def key(self, content_id, key_id):
124 | """
125 | Return a symmetric key based on a content ID and key ID
126 | """
127 | return self.derived_key(self.retrieve_content_id_secret(content_id), key_id)
128 |
--------------------------------------------------------------------------------
/src/key_server.py:
--------------------------------------------------------------------------------
1 | """
2 | http://www.apache.org/licenses/LICENSE-2.0
3 |
4 | Unless required by applicable law or agreed to in writing,
5 | software distributed under the License is distributed on an
6 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
7 | KIND, either express or implied. See the License for the
8 | specific language governing permissions and limitations
9 | under the License.
10 | """
11 |
12 | import base64
13 | import os
14 |
15 | from flask import Flask
16 | from key_server_common import ServerResponseBuilder, ServerResponseBuilderV2
17 | from key_cache import KeyCache
18 | from key_generator import KeyGenerator
19 |
20 | app = Flask(__name__)
21 |
22 | BUCKET_NAME = os.environ["KEYSTORE_BUCKET"]
23 | CLIENT_URL_PREFIX = os.environ["KEYSTORE_URL"]
24 |
25 |
26 | def server_handler(event, context):
27 | """
28 | This function is the entry point for the SPEKE reference key
29 | server Lambda. This is invoked from the API Gateway resource.
30 | """
31 | try:
32 | print(event)
33 | body = event['body']
34 | if event['isBase64Encoded']:
35 | body = base64.b64decode(body)
36 | cache = KeyCache(BUCKET_NAME, CLIENT_URL_PREFIX)
37 | generator = KeyGenerator()
38 | headers_from_event = event['headers']
39 | speke_version = headers_from_event.get('x-speke-version', '1.0')
40 |
41 | if speke_version == "2.0":
42 | response = ServerResponseBuilderV2(body, cache, generator).get_response()
43 | else:
44 | response = ServerResponseBuilder(body, cache, generator).get_response()
45 |
46 | print(response)
47 | return response
48 | except Exception as exception:
49 | print("EXCEPTION {}".format(exception))
50 | return {"isBase64Encoded": False, "statusCode": 500, "headers": {"Content-Type": "text/plain"}, "body": str(exception)}
51 |
--------------------------------------------------------------------------------
/src/zappa_settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "oss": {
3 | "app_function": "key_server.app",
4 | "aws_region": "us-east-1",
5 | "profile_name": "default",
6 | "project_name": "speke",
7 | "runtime": "python3.9",
8 | "s3_bucket": "zappa-jisselw2p"
9 | }
10 | }
--------------------------------------------------------------------------------
/tests/api_gateway_tests.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | # Apache License
4 | # Version 2.0, January 2004
5 | # http://www.apache.org/licenses/
6 |
7 | import base64
8 | import unittest
9 | import xml.etree.ElementTree as element_tree
10 | import requests
11 | from aws_requests_auth.boto_utils import BotoAWSRequestsAuth
12 |
13 | # UPDATE THIS CONSTANT TO MATCH YOUR CONFIGURATION
14 |
15 | # API_HOST = "lz9w9a2g89.execute-api.us-east-1.amazonaws.com"
16 | API_HOST = "f8aix0vkl5.execute-api.us-east-1.amazonaws.com"
17 |
18 | # static testing values and endpoints
19 | API_ENDPOINT = "https://{}/EkeStage".format(API_HOST)
20 | SERVER_API_BODY_FILE = "server_api_body.xml"
21 | API_HEADERS = {"Host": API_HOST, "Content-Type": "application/xml", "Accept": "application/xml"}
22 | SERVER_API = "{}/copyProtection".format(API_ENDPOINT)
23 | CLIENT_API = "{}/client/5E99137A-BD6C-4ECC-A24D-A3EE04B4E011/e2201617-57c2-4d9b-adc5-cd87b7c01944".format(API_ENDPOINT)
24 |
25 | # keys expected from above test data
26 | EXPECTED_SERVER_KEY = b'\x00\xbc\xcf\xd5\xa3\x93&\xfc\xdf\xaa\x0fH\xd7i6W'
27 | EXPECTED_CLIENT_KEY = b':t\x81m\xad5u\x87\xb7\x9c\x97q\xec\x07\xb0S'
28 |
29 |
30 | class TestSPEKEGateway(unittest.TestCase):
31 | """
32 | This class is responsible for testing the SPEKE API gateway.
33 | """
34 |
35 | def test_server(self):
36 | """
37 | This method tests the server REST API.
38 | """
39 | # read the XML document used by the server
40 | xml_file = open(SERVER_API_BODY_FILE, 'r')
41 | xml = xml_file.read()
42 | xml_file.close()
43 | # create a signature for the call
44 | auth = BotoAWSRequestsAuth(aws_host=API_HOST, aws_region='us-east-1', aws_service='execute-api')
45 | # call the api
46 | response = requests.post(SERVER_API, headers=API_HEADERS, data=xml, auth=auth)
47 | # get the XML response
48 | root_element = element_tree.fromstring(response.text)
49 | # check the key
50 | key = root_element.find(".//{urn:ietf:params:xml:ns:keyprov:pskc}PlainValue")
51 | self.assertEqual(EXPECTED_SERVER_KEY, base64.b64decode(key.text))
52 |
53 | def test_client(self):
54 | """
55 | This method tests the client REST API.
56 | """
57 | # send the request as a client
58 | response = requests.get(CLIENT_API, headers=API_HEADERS)
59 | # check the key response
60 | self.assertEqual(EXPECTED_CLIENT_KEY, response.content)
61 |
62 |
63 | if __name__ == '__main__':
64 | unittest.main(verbosity=2)
65 |
--------------------------------------------------------------------------------
/tests/lambda_tests.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | # Apache License
4 | # Version 2.0, January 2004
5 | # http://www.apache.org/licenses/
6 |
7 | import base64
8 | import json
9 | import unittest
10 | import xml.etree.ElementTree as element_tree
11 | import boto3
12 |
13 | # SET THESE THREE CONSTANTS TO MATCH YOUR CONFIGURATION
14 |
15 | # API_HOST = "lz9w9a2g89.execute-api.us-east-1.amazonaws.com"
16 | # DEPLOYED_REGION = "us-east-1"
17 | # SERVER_FUNCTION_NAME = "eke-server-EkeServerLambdaFunction-S68R3HGE8GTC"
18 | # CLIENT_FUNCTION_NAME = "eke-server-EkeClientLambdaFunction-1K248YEWPTU0U"
19 |
20 | API_HOST = "f8aix0vkl5.execute-api.us-east-1.amazonaws.com"
21 | DEPLOYED_REGION = "us-east-1"
22 | SERVER_FUNCTION_NAME = "eke-server-EkeServerLambdaFunction-15LUYRV00U6MP"
23 | CLIENT_FUNCTION_NAME = "eke-server-EkeClientLambdaFunction-OK9F7TPIWV3L"
24 |
25 | # static server test data -- no changes needed
26 | SERVER_FUNCTION_PAYLOAD = {
27 | "resource":
28 | "/copyProtection",
29 | "path":
30 | "/copyProtection",
31 | "httpMethod":
32 | "POST",
33 | "headers": {
34 | "Accept": "*/*",
35 | "content-type": "application/xml",
36 | "Host": API_HOST
37 | },
38 | "requestContext": {
39 | "path": "/EkeStage/copyProtection",
40 | "stage": "EkeStage",
41 | "resourcePath": "/copyProtection",
42 | "httpMethod": "POST"
43 | },
44 | "body":
45 | "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48Y3BpeDpDUElYIGlkPSI1RTk5MTM3QS1CRDZDLTRFQ0MtQTI0RC1BM0VFMDRCNEUwMTEiIHhtbG5zOmNwaXg9InVybjpkYXNoaWY6b3JnOmNwaXgiIHhtbG5zOnBza2M9InVybjppZXRmOnBhcmFtczp4bWw6bnM6a2V5cHJvdjpwc2tjIiB4bWxuczpzcGVrZT0idXJuOmF3czphbWF6b246Y29tOnNwZWtlIj48Y3BpeDpDb250ZW50S2V5TGlzdD48Y3BpeDpDb250ZW50S2V5IGtpZD0iNmM1ZjUyMDYtN2Q5OC00ODA4LTg0ZDgtOTRmMTMyYzFlOWZlIj48L2NwaXg6Q29udGVudEtleT48L2NwaXg6Q29udGVudEtleUxpc3Q+PGNwaXg6RFJNU3lzdGVtTGlzdD48Y3BpeDpEUk1TeXN0ZW0ga2lkPSI2YzVmNTIwNi03ZDk4LTQ4MDgtODRkOC05NGYxMzJjMWU5ZmUiIHN5c3RlbUlkPSI4MTM3Njg0NC1mOTc2LTQ4MWUtYTg0ZS1jYzI1ZDM5YjBiMzMiPiAgICA8Y3BpeDpDb250ZW50UHJvdGVjdGlvbkRhdGEgLz4gICAgPHNwZWtlOktleUZvcm1hdCAvPiAgICA8c3Bla2U6S2V5Rm9ybWF0VmVyc2lvbnMgLz4gICAgPHNwZWtlOlByb3RlY3Rpb25IZWFkZXIgLz4gICAgPGNwaXg6UFNTSCAvPiAgICA8Y3BpeDpVUklFeHRYS2V5IC8+PC9jcGl4OkRSTVN5c3RlbT48L2NwaXg6RFJNU3lzdGVtTGlzdD48Y3BpeDpDb250ZW50S2V5UGVyaW9kTGlzdD48Y3BpeDpDb250ZW50S2V5UGVyaW9kIGlkPSJrZXlQZXJpb2RfZTY0MjQ4ZjYtZjMwNy00Yjk5LWFhNjctYjM1YTc4MjUzNjIyIiBpbmRleD0iMTE0MjUiLz48L2NwaXg6Q29udGVudEtleVBlcmlvZExpc3Q+PGNwaXg6Q29udGVudEtleVVzYWdlUnVsZUxpc3Q+PGNwaXg6Q29udGVudEtleVVzYWdlUnVsZSBraWQ9IjZjNWY1MjA2LTdkOTgtNDgwOC04NGQ4LTk0ZjEzMmMxZTlmZSI+PGNwaXg6S2V5UGVyaW9kRmlsdGVyIHBlcmlvZElkPSJrZXlQZXJpb2RfZTY0MjQ4ZjYtZjMwNy00Yjk5LWFhNjctYjM1YTc4MjUzNjIyIi8+PC9jcGl4OkNvbnRlbnRLZXlVc2FnZVJ1bGU+PC9jcGl4OkNvbnRlbnRLZXlVc2FnZVJ1bGVMaXN0PjwvY3BpeDpDUElYPg==",
46 | "isBase64Encoded":
47 | True
48 | }
49 |
50 | # static client test data -- no changes needed
51 | CLIENT_FUNCTION_PAYLOAD = {
52 | "resource": "/client/{content_id}/{kid}",
53 | "path": "/client/5E99137A-BD6C-4ECC-A24D-A3EE04B4E011/e2201617-57c2-4d9b-adc5-cd87b7c01944",
54 | "httpMethod": "GET",
55 | "headers": {
56 | "Accept": "*/*",
57 | "Host": API_HOST,
58 | "Referer": "https://cf98fa7b2ee4450e.mediapackage.us-east-1.amazonaws.com/out/v1/5b7bf83cb49a4671aa3d6d23ad2fcacf/index.m3u8",
59 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.1.1 Safari/603.2.4"
60 | },
61 | "pathParameters": {
62 | "content_id": "5E99137A-BD6C-4ECC-A24D-A3EE04B4E011",
63 | "kid": "e2201617-57c2-4d9b-adc5-cd87b7c01944"
64 | },
65 | "requestContext": {
66 | "path": "/EkeStage/client/5E99137A-BD6C-4ECC-A24D-A3EE04B4E011/e2201617-57c2-4d9b-adc5-cd87b7c01944",
67 | "protocol": "HTTP/1.1",
68 | "stage": "EkeStage",
69 | "resourcePath": "/client/{content_id}/{kid}",
70 | "httpMethod": "GET"
71 | },
72 | "body": "",
73 | "isBase64Encoded": False
74 | }
75 |
76 | # keys expected from above test data
77 | EXPECTED_SERVER_KEY = b'\x00\xbc\xcf\xd5\xa3\x93&\xfc\xdf\xaa\x0fH\xd7i6W'
78 | EXPECTED_CLIENT_KEY = b':t\x81m\xad5u\x87\xb7\x9c\x97q\xec\x07\xb0S'
79 |
80 |
81 | class TestSPEKELambdas(unittest.TestCase):
82 | """
83 | This class is responsible for testing the Lambda used for the SPEKE server.
84 | """
85 |
86 | def test_server(self):
87 | """
88 | This class tests the server interface of the SPEKE Lambda.
89 | """
90 | client = boto3.client('lambda', region_name=DEPLOYED_REGION)
91 | response = client.invoke(
92 | FunctionName=SERVER_FUNCTION_NAME,
93 | InvocationType='RequestResponse',
94 | Payload=json.dumps(SERVER_FUNCTION_PAYLOAD),
95 | )
96 | # get the json-encoded payload
97 | stream = response["Payload"]
98 | decoded = json.loads(stream.read())
99 | stream.close()
100 | # get the XML embedded inside
101 | root_element = element_tree.fromstring(decoded["body"])
102 | # get the key element
103 | key = root_element.find(".//{urn:ietf:params:xml:ns:keyprov:pskc}PlainValue")
104 | # test the key against expected
105 | self.assertEqual(EXPECTED_SERVER_KEY, base64.b64decode(key.text))
106 |
107 | def test_client(self):
108 | """
109 | This class tests the client interface of the SPEKE Lambda.
110 | """
111 | client = boto3.client('lambda', region_name=DEPLOYED_REGION)
112 | response = client.invoke(
113 | FunctionName=CLIENT_FUNCTION_NAME,
114 | InvocationType='RequestResponse',
115 | Payload=json.dumps(CLIENT_FUNCTION_PAYLOAD),
116 | )
117 | # get the json-encoded payload
118 | stream = response["Payload"]
119 | decoded = json.loads(stream.read())
120 | stream.close()
121 | # get the key
122 | key = decoded["body"]
123 | # test the key against expected
124 | self.assertEqual(EXPECTED_CLIENT_KEY, base64.b64decode(key))
125 |
126 |
127 | if __name__ == '__main__':
128 | unittest.main(verbosity=2)
129 |
--------------------------------------------------------------------------------
/tests/requirements.txt:
--------------------------------------------------------------------------------
1 | argcomplete==1.11.1
2 | asn1crypto==1.3.0
3 | astroid==2.3.3
4 | autopep8==1.5
5 | aws-requests-auth==0.4.2
6 | awscli==1.18.31
7 | awslogs==0.11.0
8 | base58==2.0.0
9 | boto==2.49.0
10 | boto3==1.12.31
11 | botocore==1.15.31
12 | certifi==2022.12.7
13 | cffi==1.14.0
14 | cfn-flip==1.2.2
15 | chardet==3.0.4
16 | click==7.1.1
17 | colorama==0.4.3
18 | cryptography==41.0.0
19 | docutils==0.16
20 | durationpy==0.5
21 | Flask==2.3.2
22 | hjson==3.0.1
23 | idna==2.9
24 | importlib-metadata==1.6.0
25 | isort==4.3.21
26 | itsdangerous==1.1.0
27 | jedi==0.16.0
28 | Jinja2==2.11.3
29 | jmespath==0.9.5
30 | kappa==0.7.0
31 | lambda-packages==0.20.0
32 | lazy-object-proxy==1.4.3
33 | MarkupSafe==1.1.1
34 | mccabe==0.6.1
35 | parso==0.6.2
36 | pep8==1.7.1
37 | pip-tools==4.5.1
38 | placebo==0.9.0
39 | pyasn1==0.4.8
40 | pycodestyle==2.5.0
41 | pycparser==2.20
42 | pylint==2.4.4
43 | python-dateutil==2.8.1
44 | python-slugify==4.0.0
45 | PyYAML==5.4
46 | requests==2.31.0
47 | rope==0.16.0
48 | rsa==4.7
49 | s3transfer==0.3.3
50 | six==1.14.0
51 | termcolor==1.1.0
52 | text-unidecode==1.3
53 | toml==0.10.0
54 | tqdm==4.44.1
55 | troposphere==2.6.0
56 | typed-ast==1.5.4
57 | Unidecode==1.1.1
58 | urllib3==1.26.5
59 | Werkzeug==2.2.3
60 | wrapt==1.12.1
61 | wsgi-request-logger==0.4.6
62 | yapf==0.29.0
63 | zappa==0.51.0
64 | zipp==3.1.0
65 |
--------------------------------------------------------------------------------
/tests/server_api_body.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/tests/server_api_body_spekev2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/workflow/drm-live.md:
--------------------------------------------------------------------------------
1 | # Module: Digital Rights Management (DRM) and Encryption
2 |
3 | When working with videos for your service or Over the Top (OTT) platform, you will very likely need to secure and protect your live streams prior to delivering content to your end users. Approaches for securing content include basic content _encryption_ or by applying highly secure Digital Rights Management (DRM) to the content. Examples of DRM include Fairplay, Widevine and PlayReady.
4 |
5 | In this module, you'll use AWS Elemental MediaPackage, to secure and encrypt your live channels. You'll learn about the Secure Packager and Encoder Key Exchange (SPEKE) API, deploy an AWS SPEKE reference server, and configure AWS Elemental MediaPackage to encrypt HLS content using AES-128 encryption.
6 |
7 | ## Prerequisites
8 | You'll need to have previously installed the Live Streaming Solution
9 |
10 | You'll need to have previously deployed the AWS SPEKE Reference Server.
11 | https://github.com/awslabs/speke-reference-server
12 |
13 | Once you've installed the AWS SPEKE Reference Server retrieve the SPEKE API URL and MediaPackage Role from the output of your Cloudformation Stack Details.
14 |
15 | Goto CloudFormation-> Stacks -> **AWS SPEKE Reference Server Stack Name** -> Outputs
16 | and make a note of the below paramters
17 |
18 | | Parameter | Example |
19 | |--------------------------|-------------------------------------------------------------------------------------------|
20 | | SPEKEServerURL |``` https://{hostname}.execute-api.eu-west-1.amazonaws.com/EkeStage/copyProtection ``` |
21 | | MediaPackageSPEKERoleArn|``` arn:aws:iam::{AWS_ACCOUNT}:role/speke-reference-MediaPackageInvokeSPEKERole-{INSTANCE_ID} ``` |
22 |
23 | Next, Goto CloudFormation -> **Live Streaming Solution Stack** -> Outputs and make a note of the parameters below.
24 |
25 | | Parameter | |
26 | |--------------------------|-------------------------------------------------------------------------------------------|
27 | | DemoConsole |``` https://{host}.cloudfront.net/index.html ``` |
28 |
29 | **Make sure you replace the various values such as hostname, aws_account with your own deployment vaues**
30 |
31 | ### API Gateway
32 |
33 | #### Server Test
34 |
35 | 1. Navigate to the AWS API Gateway Console
36 | 1. Select the region deployed with the SPEKE Reference Server
37 | 1. Select the SPEKEReferenceAPI
38 | 1. Select the POST method on the /copyProtection resource
39 | 1. Click the Test link on the left side of the main compartment
40 | 1. Copy the following into the Request Body compartment
41 | ```
42 |
43 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | ```
70 | 7. Click the Test button
71 | 8. Confirm that you have a similar response returned by the API request.
72 | ```
73 |
74 |
75 | ALzP1aOTJvzfqg9I12k2Vw==
76 |
77 |
78 |
79 |
80 |
81 | aHR0cHM6Ly9kMnVod2Jqc3p1ejF2Ny5jbG91ZGZyb250Lm5ldC81RTk5MTM3QS1CRDZDLTRFQ0MtQTI0RC1BM0VFMDRCNEUwMTEvNmM1ZjUyMDYtN2Q5OC00ODA4LTg0ZDgtOTRmMTMyYzFlOWZl
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | ```
94 | ## 2. Configuring DRM on a MediaPackage EndPoint
95 |
96 | 1. Login to the AWS Console
97 | 1. Navigate to *MediaPackage*
98 | 1. Select the **reinvent-live-livestream** channel
99 | 1. Scroll down to *Endpoints* section of the channel details
100 |
101 | 
102 |
103 | 5. Select the **reinvent-live-livestream-hls** endpoint and *edit* the endpoint
104 | 1. Scroll down to the *Package encryption* section of the endpoint details
105 | 1. Select the **Encrypt Content** radio button
106 | 1. Fill in the following encryption details
107 | ResourceID : ```6c5f5206-7d98-4808-84d8-94f132c1e9fe```
108 | DRM System ID : ```81376844-f976-481e-a84e-cc25d39b0b33```
109 | URL : ``` { SPEKEServerURL }```
110 | MediaPackage Role : ```{MediaPackage Role from the Stack Output }```
111 | 1. Expand the *additional configuration*
112 | 1. Select `AES 128` for the Encryption method.
113 |
114 | 
115 | 11. Click on **Save** to update your changes.
116 |
117 | ## 3. Play the videos
118 |
119 | 
120 |
121 | You can play the AES-128 encrypted HLS endpoint using:
122 |
123 | * Open up the **DemoConsole** URL in a browser. **DemoConsole** is outlined in the output of the Live Solution CloudFormation Stack.
124 | * Click on the **Preview** for the **HLS** Endpoint
125 |
126 |
127 | 
128 |
129 | ## Completion
130 |
131 | Congratulations! You have successfully created an encrypted live stream using AWS Elemental MediaPackage.
132 |
133 |
134 |
135 |
--------------------------------------------------------------------------------
/workflow/drm-vod.md:
--------------------------------------------------------------------------------
1 | # Module: Digital Rights Management (DRM) and Encryption
2 |
3 | When working with videos for your service or Over the Top (OTT) platform, you will very likely need to secure and protect your content prior to delivering videos to your end users. Approaches for securing content include basic content _encryption_ or by applying highly secure Digital Rights Management (DRM) to the content. Examples of DRM include Fairplay, Widevine and PlayReady.
4 |
5 | In this module, you'll use AWS Elemental MediaConvert, a file-based video transcoding service to secure and encrypt your videos. You'll learn about the Secure Packager and Encoder Key Exchange (SPEKE) API, deploy an AWS SPEKE reference server, and configure AWS Elemental MediaConvert to encrypt HLS packaged content using AES-128 encryption.
6 |
7 | ## Prerequisites
8 | You'll need to have previously deployed the AWS SPEKE Reference Server.
9 | https://github.com/awslabs/speke-reference-server
10 |
11 | Goto CloudFormation-> Stacks -> **AWS SPEKE Reference Server Stack Name** -> Outputs
12 | and make a note of the below parameters
13 |
14 | | Parameter | Example |
15 | |--------------------------|-------------------------------------------------------------------------------------------|
16 | | SPEKEServerURL |``` https://{HOST}.execute-api.eu-west-1.amazonaws.com/EkeStage/copyProtection ``` |
17 |
18 |
19 | ## 1. Testing the SPEKE API...
20 |
21 | ### API Gateway
22 |
23 | #### Server Test
24 |
25 | 1. Navigate to the AWS API Gateway Console
26 | 1. Select the region deployed with the SPEKE Reference Server
27 | 1. Select the SPEKEReferenceAPI
28 | 1. Select the POST method on the /copyProtection resource
29 | 1. Click the Test link on the left side of the main compartment
30 | 1. Copy the following into the Request Body compartment
31 | ```
32 |
33 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | ```
60 | 6. Click the Test button
61 | 7. Confirm that you have a similar response returned by the API request.
62 | ```
63 |
64 |
65 | ALzP1aOTJvzfqg9I12k2Vw==
66 |
67 |
68 |
69 |
70 |
71 | aHR0cHM6Ly9kMnVod2Jqc3p1ejF2Ny5jbG91ZGZyb250Lm5ldC81RTk5MTM3QS1CRDZDLTRFQ0MtQTI0RC1BM0VFMDRCNEUwMTEvNmM1ZjUyMDYtN2Q5OC00ODA4LTg0ZDgtOTRmMTMyYzFlOWZl
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | ```
84 | ## 2. Configuring DRM for a MediaConvert Job
85 |
86 | A MediaConvert Output group setting lets you configure the DRM parameters required to encrypt a video for that job. Please note : that you can currently have one package format e.g HLS or DASH configured per Job Template. In addition you can have upto 2 DRM(s) applied to an output group. For example DASH allows for Widevine and PlayReady DRM to be used for a single video.
87 |
88 | In this module, you will edit an existing MediaConvert Job Template and update it to encrypt your video using AES-128 encryption using the AWS Speke Reference Server.
89 |
90 | ### Detailed Instructions
91 |
92 | #### Job Templates section
93 |
94 | 1. Open the MediaConvert console for the region you are completing the lab in (eu-west-1 Ireland).
https://eu-west-1.console.aws.amazon.com/mediaconvert
95 | 1. Select **Job templates** from the side bar menu.
96 | 1. Select **Custom Templates** from the dropdown menu
97 |
98 | 
99 |
100 | 4. Select `{stack}_Ott_1080p_Avc_Aac_16x9_hls_encryption` to open the Jobs templates details page.
101 | 1. Click on **Update** to edit the Template
102 | 1. Select the 'Apple HLS ' Output Group underneath the Output group Panel
103 | 1. Turn on **DRM encryption**
104 |
105 | 
106 |
107 | 8. Select `AES 128` for the Encryption method.
108 | 1. Select `SPEKE` as the Key provider type.
109 | 1. Enter this for ResourceID
110 | ```
111 | 6c5f5206-7d98-4808-84d8-94f132c1e9fe
112 | ```
113 | 1. Enter this DRM System ID for AES-128
114 | ```
115 | 81376844-f976-481e-a84e-cc25d39b0b33
116 | ```
117 |
118 | 12. Enter your SPEKE Reference Server API as the URL. ( Replace the Hostname )
119 | ```
120 | https://{host}.execute-api.eu-west-1.amazonaws.com/EkeStage/copyProtection
121 | ```
122 |
123 | 
124 |
125 | 13. Click on **Update** at the bottom of the page to save the Job template.
126 |
127 | ## 3. Resubmit / Reprocess the Video Asset with Encryption
128 |
129 |
130 | ### Update Lambda to use the Encryption Template
131 | 1. In the AWS Management Console, navigate to AWS Lambda
132 |
133 | 
134 |
135 | 1. Select the ```{stackname}-input-validate``` function and scroll down to the enviornment variable
136 | 1. Look for the ```MediaConvert_Template_1080p``` parameter and replace it with **{stackname}_Ott_1080p_Avc_Aac_16x9_hls_encryption**
137 | 1. Click on the **Save** Button
138 |
139 | 
140 |
141 | ### Trigger Workflow by renaming source asset.
142 | 1. In the AWS Management Console choose **Services** then select **S3** under Storage.
143 | 1. Select the bucket where your source input files are located ```{stack}-source```
144 | 1. Rename the source asset ```beach_aerial_short.mp4 ``` to ```beach_aerial_short_1.mp4 ``` by right clicking the on the filename
145 | 1. This should trigger an asset workflow and the encrypted files will be output to a folder
146 |
147 |
148 | ## 4. Confirm MediaConvert Job Completion
149 |
150 | 1. In the AWS Management Console choose **AWS MediaConvert** then select **Jobs** from the righthand menu
151 | 1. You should see a newly submitted MediaConvert job in a PROGRESSING or COMPELTE state
152 | 1. Once your Job is complete you should now be able to playback the encoded assets.
153 | 1. Keep track of the MediaConvert Job ID which will be used to lookup the HLS Playback URL.
154 |
155 | ## 5. Play the videos
156 |
157 | You should have received an email with a link to the HLS-128 encrypted asset upon completion of the workflow.
158 |
159 | ### Alternatively - Lookup the HLS URL from Amazon DynamoDB
160 |
161 | 1. In the AWS Management Console choose **Services** then select **DynamoDB** under Databases.
162 | 1. Select the {stack-name} Table and Choose Items
163 | 1. Find the GUID based looking up on the Elemental MediaConvert JobID under the **ecodeJobId** coloumn
164 | 1. Copy the corresponding **hlsURL** value
165 |
166 |
167 | You can play the HLS streaming using:
168 | * DemoConsole Player
169 | 1. Go the landing page for the Video On Demand workshop
170 | 1. Click on the Preview link to load the Video Player
171 | 1. Paste and preview the HLS Url into the Cloudfront Url input box.
172 | * or Open Safari on a Mac and paste the HLS URL into the browser.
173 |
174 |
175 | ## Completion
176 |
177 | Congratulations! You have successfully created an encrypted video asset using AWS Elemental MediaConvert.
178 |
--------------------------------------------------------------------------------
/workflow/images/live_mediapackage-encryption_config.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/speke-reference-server/5dc9c382532e5c3786ebae3fe540d5ff6ecb6de5/workflow/images/live_mediapackage-encryption_config.png
--------------------------------------------------------------------------------
/workflow/images/live_mediapackage-endpoints.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/speke-reference-server/5dc9c382532e5c3786ebae3fe540d5ff6ecb6de5/workflow/images/live_mediapackage-endpoints.png
--------------------------------------------------------------------------------
/workflow/images/live_mediapackage-preview-hls.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/speke-reference-server/5dc9c382532e5c3786ebae3fe540d5ff6ecb6de5/workflow/images/live_mediapackage-preview-hls.png
--------------------------------------------------------------------------------
/workflow/images/live_mediapackage_drm_config.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/speke-reference-server/5dc9c382532e5c3786ebae3fe540d5ff6ecb6de5/workflow/images/live_mediapackage_drm_config.png
--------------------------------------------------------------------------------
/workflow/images/vod_custom_templates.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/speke-reference-server/5dc9c382532e5c3786ebae3fe540d5ff6ecb6de5/workflow/images/vod_custom_templates.png
--------------------------------------------------------------------------------
/workflow/images/vod_drm_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/speke-reference-server/5dc9c382532e5c3786ebae3fe540d5ff6ecb6de5/workflow/images/vod_drm_settings.png
--------------------------------------------------------------------------------
/workflow/images/vod_hls_output_group.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/speke-reference-server/5dc9c382532e5c3786ebae3fe540d5ff6ecb6de5/workflow/images/vod_hls_output_group.png
--------------------------------------------------------------------------------
/workflow/images/vod_lambda_input_validate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/speke-reference-server/5dc9c382532e5c3786ebae3fe540d5ff6ecb6de5/workflow/images/vod_lambda_input_validate.png
--------------------------------------------------------------------------------
/workflow/images/vod_lambda_template.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/speke-reference-server/5dc9c382532e5c3786ebae3fe540d5ff6ecb6de5/workflow/images/vod_lambda_template.png
--------------------------------------------------------------------------------
/yapf.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # http://www.apache.org/licenses/LICENSE-2.0
4 | #
5 | # Unless required by applicable law or agreed to in writing,
6 | # software distributed under the License is distributed on an
7 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
8 | # KIND, either express or implied. See the License for the
9 | # specific language governing permissions and limitations
10 | # under the License.
11 |
12 | find . -iname '*.py' -print0 | \
13 | xargs -0 yapf -i --style='{based_on_style: pep8, join_multiple_lines: true, column_limit: 200, indent_width: 4}'
14 |
--------------------------------------------------------------------------------