|
| 1 | +# User-managed encryption key publisher |
| 2 | + |
| 3 | +Code intended to be run by users of the Live Stream API. This code utilizes DRM |
| 4 | +provider CPIX APIs to fetch encryption keys and store them in Google Secret |
| 5 | +Manager for use with the Live Stream API's encryption feature. |
| 6 | + |
| 7 | +To create a zip archive of all the files needed to run key publisher, run the |
| 8 | +script `./package.sh`. |
| 9 | + |
| 10 | +## 1. Setup |
| 11 | + |
| 12 | +### Install and configure gcloud |
| 13 | + |
| 14 | +Follow [standard install instructions](http://cloud/sdk/docs/install). Use |
| 15 | +`gcloud config` to set your project and preferred region/zone. |
| 16 | + |
| 17 | +## 2. Configure Cloud Functions and API Gateway |
| 18 | + |
| 19 | +### Enable APIs |
| 20 | + |
| 21 | +Enable the following Google APIs to host your key publisher and write key |
| 22 | +information to Secret Manager. |
| 23 | + |
| 24 | +```shell |
| 25 | +gcloud services enable apigateway.googleapis.com |
| 26 | +gcloud services enable cloudbuild.googleapis.com |
| 27 | +gcloud services enable cloudfunctions.googleapis.com |
| 28 | +gcloud services enable secretmanager.googleapis.com |
| 29 | +gcloud services enable servicecontrol.googleapis.com |
| 30 | +gcloud services enable servicemanagement.googleapis.com |
| 31 | +``` |
| 32 | + |
| 33 | +### Create service accounts |
| 34 | + |
| 35 | +Two service accounts are needed for your key publisher. The first allows your |
| 36 | +API Gateway to invoke your cloud function (with the IAM role of |
| 37 | +`roles/cloudfunctions.invoker`), and the second allows Cloud Functions to read |
| 38 | +and write to Secret Manager (with the IAM role of `roles/secretmanager.admin`). |
| 39 | + |
| 40 | +Create the service account for invoking Cloud Functions: |
| 41 | + |
| 42 | +```shell |
| 43 | +gcloud iam service-accounts create livestream-cf-invoker \ |
| 44 | + --display-name="Cloud Function invoker for the API Gateway" \ |
| 45 | + --description="Service Account to be used by the API Gateway to invoke Cloud Functions" |
| 46 | +``` |
| 47 | + |
| 48 | +Then configure the IAM policy binding using your `project-id`: |
| 49 | + |
| 50 | +```shell |
| 51 | +gcloud projects add-iam-policy-binding [MY-PROJEC-ID] \ |
| 52 | + --member serviceAccount:"livestream-cf-invoker@[MY-PROJECT-ID].iam.gserviceaccount.com" \ |
| 53 | + --role "roles/cloudfunctions.invoker" \ |
| 54 | + --no-user-output-enabled \ |
| 55 | + --quiet |
| 56 | +``` |
| 57 | + |
| 58 | +Next, create another service account for interacting with Secret Manager: |
| 59 | + |
| 60 | +```shell |
| 61 | +gcloud iam service-accounts create livestream-cloud-functions \ |
| 62 | + --display-name="Service account for Cloud Functions" \ |
| 63 | + --description="Service Account used for the Live Stream API key publisher functions" |
| 64 | +``` |
| 65 | + |
| 66 | +Then configure the IAM policy binding: |
| 67 | + |
| 68 | +```shell |
| 69 | +gcloud projects add-iam-policy-binding [MY-PROJECT-ID] \ |
| 70 | + --member serviceAccount:"livestream-cloud-functions@[MY-PROJECT-ID].iam.gserviceaccount.com" \ |
| 71 | + --role "roles/secretmanager.admin" \ |
| 72 | + --no-user-output-enabled \ |
| 73 | + --quiet |
| 74 | +``` |
| 75 | + |
| 76 | +### Deploy the cloud function |
| 77 | + |
| 78 | +Create a file `.env.yaml`. Take a look at the example template file: |
| 79 | +[`env.template.yml`](./templates/env.template.yml). Copy the template file and |
| 80 | +replace any needed values. This file will store environment variables to be used |
| 81 | +by your function. This file should have the following contents (also included in |
| 82 | +the example file): |
| 83 | + |
| 84 | +```yaml |
| 85 | +# Your project ID. |
| 86 | +PROJECT: [MY-PROJECT-ID] |
| 87 | +# The endpoint of the CPIX API the key publisher will be calling. |
| 88 | +CPIX_ENDPOINT: https://cpix-api.url |
| 89 | +# The ID of the secret containing the provider's public certificate. |
| 90 | +CPIX_PUBLIC_CERT_PROVIDER_SECRET: cpix-public-cert-provider |
| 91 | +# The ID of the secret containing your private key. |
| 92 | +CPIX_PRIVATE_KEY_USER_SECRET: cpix-private-key-user |
| 93 | +# The ID of the secret containing your public certificate. |
| 94 | +CPIX_PUBLIC_CERT_USER_SECRET: cpix-public-cert-user |
| 95 | +# (Optional) The ID of the secret containing your enduser private key. |
| 96 | +CPIX_PRIVATE_KEY_ENDUSER_SECRET: cpix-private-key-enduser |
| 97 | +# (Optional) The ID of the secret containing your enduser public certificate. |
| 98 | +CPIX_PUBLIC_CERT_ENDUSER_SECRET: cpix-public-cert-enduser |
| 99 | +``` |
| 100 | +
|
| 101 | +Then deploy your function: |
| 102 | +
|
| 103 | +```shell |
| 104 | +gcloud functions deploy livestream-key-publisher \ |
| 105 | + --entry-point=keys \ |
| 106 | + --runtime=python310 \ |
| 107 | + --trigger-http \ |
| 108 | + --memory=256MB \ |
| 109 | + --security-level=secure-always \ |
| 110 | + --timeout=60s \ |
| 111 | + --env-vars-file=.env.yaml \ |
| 112 | + --service-account="livestream-cloud-functions@[MY-PROJECT-ID].iam.gserviceaccount.com" |
| 113 | +``` |
| 114 | + |
| 115 | +After your function is deployed, the response you get will contain the |
| 116 | +`httpsTrigger.url` field. It will show up in the following format: |
| 117 | + |
| 118 | +```shell |
| 119 | +httpsTrigger: |
| 120 | + url: https://<REGION>-<MY-PROJECT_ID>.cloudfunctions.net/livestream-key-publisher |
| 121 | +``` |
| 122 | + |
| 123 | +Save this value for later when you configure your API Gateway. |
| 124 | + |
| 125 | +### Create the API and API Gateway |
| 126 | + |
| 127 | +Create the API: |
| 128 | + |
| 129 | +```shell |
| 130 | +gcloud api-gateway apis create livestream-key-publisher-api --display-name="Live Stream Key Publisher API" |
| 131 | +``` |
| 132 | + |
| 133 | +Create a file `api-config.yml`. Take a look at the example template file: |
| 134 | +[`api-config.template.yml`](./templates/api-config.template.yml). Copy the |
| 135 | +template file and replace any needed values. In particular `CLOUD_FUNCTION_URL` |
| 136 | +must be replaced with the URL you received when you deployed your function. |
| 137 | + |
| 138 | +Once the file has been created, create your API config: |
| 139 | + |
| 140 | +```shell |
| 141 | +gcloud api-gateway api-configs create livestream-key-publisher-api-config-1 \ |
| 142 | + --display-name="Live Stream Key Publisher API Config v1" \ |
| 143 | + --api=livestream-key-publisher-api \ |
| 144 | + --openapi-spec="api-config.yml" \ |
| 145 | + --backend-auth-service-account="livestream-cf-invoker@[MY-PROJECT-ID].iam.gserviceaccount.com" |
| 146 | +``` |
| 147 | + |
| 148 | +Next, create the API Gateway. The following example deploys to GCP Region |
| 149 | +`us-central1`. You can choose a different location from supported GCP Regions |
| 150 | +[listed here](https://cloud.google.com/api-gateway/docs/deploying-api#deploy_an_api_config_to_a_gateway): |
| 151 | + |
| 152 | +```shell |
| 153 | +gcloud api-gateway gateways create livestream-key-publisher-api-gateway \ |
| 154 | + --display-name="Live Stream Key Publisher API Gateway" \ |
| 155 | + --api=livestream-key-publisher-api \ |
| 156 | + --api-config=livestream-key-publisher-api-config-1 \ |
| 157 | + --location=us-central1 |
| 158 | +``` |
| 159 | + |
| 160 | +Once created, your API must be enabled. Run a command to describe your API: |
| 161 | + |
| 162 | +```shell |
| 163 | +gcloud api-gateway apis describe livestream-key-publisher-api |
| 164 | +``` |
| 165 | + |
| 166 | +You will get the `managedService` field in the response. This is the service you |
| 167 | +need to enable. For example, |
| 168 | + |
| 169 | +```shell |
| 170 | +gcloud services enable livestream-key-publisher-api-144h6p0e7bzc1.apigateway.[MY-PROJECT-ID].cloud.goog |
| 171 | +``` |
| 172 | + |
| 173 | +Now you need to locate the endpoint URL of your API. Run a command to describe |
| 174 | +your gateway: |
| 175 | + |
| 176 | +```shell |
| 177 | +gcloud api-gateway gateways describe livestream-key-publisher-api-gateway --location=us-central1 |
| 178 | +``` |
| 179 | + |
| 180 | +You will get the `defaultHostname` field in the response. For example, |
| 181 | + |
| 182 | +``` |
| 183 | +defaultHostname: livestream-key-publisher-api-gateway-48mwfke9.uc.gateway.dev |
| 184 | +``` |
| 185 | + |
| 186 | +You will send API requests to the URL received from your response. |
| 187 | + |
| 188 | +Create an API key to use for making queries to your API, and restrict the API |
| 189 | +key such that only your newly created `livestream-key-publisher-api` can be |
| 190 | +called with the API key. Specify the API restriction with the `managedService` |
| 191 | +field from earlier: |
| 192 | + |
| 193 | +```shell |
| 194 | +gcloud alpha services api-keys create --api-target=service=[managedService] |
| 195 | +``` |
| 196 | + |
| 197 | +For example |
| 198 | + |
| 199 | +```shell |
| 200 | +gcloud alpha services api-keys create --api-target=service=livestream-key-publisher-api-144h6p0e7bzc1.apigateway.[MY-PROJECT-ID].cloud.goog |
| 201 | +``` |
| 202 | + |
| 203 | +> **Warning**: You will get the `keyString` field in the response. This is your |
| 204 | +> API key. Save this key, as you will not be able to retrieve it again. |
| 205 | +
|
| 206 | +### Test your API |
| 207 | + |
| 208 | +Using the above information, use `curl` to make a `GET` query to your API: |
| 209 | + |
| 210 | +```shell |
| 211 | +curl --location --request POST \ |
| 212 | + 'https://livestream-key-publisher-api-gateway-48mwfke9.uc.gateway.dev/keys?api_key=YOUR_API_KEY' \ |
| 213 | + --header 'Content-Type: application/json' \ |
| 214 | + --data '{"mediaId": "my-asset", "provider": "FakeProvider", "keyIds": ["abcd1234", "efgi5678"]}' |
| 215 | +``` |
| 216 | + |
| 217 | +Arguments in the request body are: |
| 218 | + |
| 219 | +* `mediaId`: arbitrary identifier for the media being encrypted. |
| 220 | +* `provider`: the DRM provider to use. Omit this argument to see a list of |
| 221 | + supported providers. |
| 222 | +* `keyIds`: a list of key IDs to prepare for the given media ID. |
| 223 | + |
| 224 | +This example query uses `FakeProvider`. This is a provider used as a reference |
| 225 | +example, which does not make any actual CPIX queries, but generates random hex |
| 226 | +strings in place of a real key. These keys will not work for a real encryption |
| 227 | +setup. `keyIds` should be unique identifiers for the keys provided by your |
| 228 | +third-party DRM provider. Please replace `provider` and `keyIds` accordingly. |
| 229 | +For `mediaId`, you can change it to any identifier to label the encrypted media. |
| 230 | + |
| 231 | +If the query is successful, you will see a response like: |
| 232 | + |
| 233 | +``` |
| 234 | +projects/PROJECT_NUMBER/secrets/MEDIA_ID/versions/1 |
| 235 | +``` |
| 236 | + |
| 237 | +This is the name of the secret version that was created to hold your encryption |
| 238 | +keys. |
| 239 | + |
| 240 | +To verify this, you can use `gcloud` to access the content of that secret: |
| 241 | + |
| 242 | +```shell |
| 243 | +gcloud secrets versions access projects/PROJECT_NUMBER/secrets/MEDIA_ID/versions/1 |
| 244 | +``` |
| 245 | + |
| 246 | +## 3. Update code |
| 247 | + |
| 248 | +Use `gcloud functions deploy` to update your Cloud Function code. Ensure |
| 249 | +`.env.yaml` still exists (example file: |
| 250 | +[env.template.yml](./templates/env.template.yml)). |
| 251 | + |
| 252 | +```shell |
| 253 | +gcloud functions deploy livestream-key-publisher \ |
| 254 | + --entry-point=keys \ |
| 255 | + --runtime=python310 \ |
| 256 | + --trigger-http \ |
| 257 | + --memory=256MB \ |
| 258 | + --security-level=secure-always \ |
| 259 | + --timeout=60s \ |
| 260 | + --env-vars-file=.env.yaml \ |
| 261 | + --service-account="livestream-cloud-functions@[MY-PROJECT-ID].iam.gserviceaccount.com" |
| 262 | +``` |
| 263 | + |
| 264 | +If request paths or parameters have changed, you may also need to update |
| 265 | +`api-config.yml` to match. To do this, update `api-config.yml` as needed, then |
| 266 | +create a new API config: |
| 267 | + |
| 268 | +```shell |
| 269 | +gcloud api-gateway api-configs create livestream-key-publisher-api-config-2 \ |
| 270 | + --display-name="Live Stream Key Publisher API Config v2" \ |
| 271 | + --api=livestream-key-publisher-api \ |
| 272 | + --openapi-spec="api-config.yml" \ |
| 273 | + --backend-auth-service-account="livestream-cf-invoker@[MY-PROJECT-ID].iam.gserviceaccount.com" |
| 274 | +``` |
| 275 | + |
| 276 | +Once the config has been created, update the API Gateway to use your new config: |
| 277 | + |
| 278 | +```shell |
| 279 | +gcloud api-gateway gateways update livestream-key-publisher-api-gateway \ |
| 280 | + --api=livestream-key-publisher-api \ |
| 281 | + --api-config=livestream-key-publisher-api-config-2 \ |
| 282 | + --location=us-central1 |
| 283 | +``` |
| 284 | + |
| 285 | +## 4. Supported Python Versions |
| 286 | + |
| 287 | +The code samples for key publisher are compatible with all current |
| 288 | +[active](https://devguide.python.org/developer-workflow/development-cycle/index.html#in-development-main-branch) |
| 289 | +and |
| 290 | +[maintenance](https://devguide.python.org/developer-workflow/development-cycle/index.html#maintenance-branches) |
| 291 | +versions of Python. |
| 292 | + |
| 293 | +## 5. Testing |
| 294 | + |
| 295 | +You can run unit tests for this code in a local environment. These tests do not |
| 296 | +utilize external services (e.g. Google Cloud Functions, Google Secret Manager). |
| 297 | + |
| 298 | +Install python virtual environment: |
| 299 | + |
| 300 | +```shell |
| 301 | +sudo apt-get install python3-venv |
| 302 | +``` |
| 303 | + |
| 304 | +Create a local virtual environment for running tests: |
| 305 | + |
| 306 | +```shell |
| 307 | +python3 -m venv venv-key-publisher-test |
| 308 | +``` |
| 309 | + |
| 310 | +And activate the virtual environment: |
| 311 | + |
| 312 | +```shell |
| 313 | +. venv-key-publisher-test/bin/activate |
| 314 | +``` |
| 315 | + |
| 316 | +Install dependencies needed for testing: |
| 317 | + |
| 318 | +```shell |
| 319 | +pip install -r requirements.txt |
| 320 | +``` |
| 321 | + |
| 322 | +Execute the tests: |
| 323 | + |
| 324 | +```shell |
| 325 | +pytest -k _test.py -v |
| 326 | +``` |
0 commit comments