├── .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 | ![Image of serverless SPEKE](images/speke-reference.png) 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 | ![Image of Use Case View](use-cases.jpg) 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 | ![Image of Logical View](logical-view.jpg) 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 | ![Image of Deployment View](deployment-view.jpg) 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 | ![Image of Physical View](physical-view.jpg) 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 | ![Image of Key Generation](bv-key-generation.jpg) 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 | ![Image of Key Retrieval](bv-key-retrieval.jpg) 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>(.*)" 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 | ![s3 link](./images/live_mediapackage-endpoints.png) 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 | ![s3 link](./images/live_mediapackage_drm_config.png) 115 | 11. Click on **Save** to update your changes. 116 | 117 | ## 3. Play the videos 118 | 119 | ![s3 link](./images/live_mediapackage-encryption_config.png) 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 | ![s3 link](./images/live_mediapackage-preview-hls.png) 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 | ![Select Job Templates](./images/vod_custom_templates.png) 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 | ![HLS Output Group](./images/vod_hls_output_group.png) 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 | ![DRM_Settings](./images/vod_drm_settings.png) 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 | ![Look for Input_Validate Function_in_Lambda](./images/vod_lambda_input_validate.png) 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 | ![Replace_Template_in_Lambda](./images/vod_lambda_template.png) 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 | --------------------------------------------------------------------------------