feat(remote): verify server-CDM vault keys on the client and report bad pairs

The licence response lists KIDs a server vault supplied, the client proves them with the decode check, and a bad pair goes to the new session keys/bad route so the server flags its own row. Client vault keys go first over --remote, and the dashboard health warns when no SQLite vault can store flags.
This commit is contained in:
imSp4rky
2026-09-12 15:13:08 -06:00
parent 7c82282813
commit a7d6791136
21 changed files with 579 additions and 37 deletions
+4 -2
View File
@@ -210,8 +210,10 @@ missing or a vault's credentials expired. This finds that before a job does.
`status` is `ok`, `degraded` (an optional dependency is missing) or `failing` (a required one
is). Player binaries are not checked: a headless server never has one and it plays nothing.
Each check is `ok`, `warn` or `fail`. Check ids are the binary names, `cdm`,
`vault:<name>` (the vault's configured `name`, so two vaults of the same type stay apart), and
`proxies`.
`vault:<name>` (the vault's configured `name`, so two vaults of the same type stay apart),
`bad_keys` and `proxies`. `bad_keys` warns when the vaults hold no `SQLite` entry, because only
that backend stores the flag for a content key a remote client proved wrong; without it the
server serves the same bad pair again.
The server caches the result for 30 seconds and keeps every probe shallow: a vault answers one
lookup that should miss, and no probe allocates anything. A failing probe's `detail` has the
+18 -2
View File
@@ -849,6 +849,7 @@ POST /api/session/{id}/tracks → tracks + chapters (with URLs) for one title
POST /api/session/{id}/segments → resolve segment URLs / DRM / headers per track
POST /api/session/{id}/segment_filter → HLS segments the service drops (ads, bumpers)
POST /api/session/{id}/license → proxy or server-CDM DRM licensing
POST /api/session/{id}/keys/bad → report a server-vault content key that did not decrypt
DELETE /api/session/{id} → tear down, harvest updated cache
```
@@ -1058,13 +1059,13 @@ Get the content keys for the DRM. The `mode` field selects one of two modes.
=== "Single: Response `200`"
```json
{ "keys": { "<kid_hex>": "<key_hex>" } }
{ "keys": { "<kid_hex>": "<key_hex>" }, "vault_keys": ["<kid_hex>"] }
```
=== "Batch: Response `200`"
```json
{ "keys": { "v-1": { "<kid_hex>": "<key_hex>" } }, "drm_type": "widevine" }
{ "keys": { "v-1": { "<kid_hex>": "<key_hex>" } }, "vault_keys": ["<kid_hex>"], "drm_type": "widevine" }
```
| Field | Description |
@@ -1074,6 +1075,7 @@ Get the content keys for the DRM. The `mode` field selects one of two modes.
| `pssh` | Base64 PSSH (server-CDM mode). |
| `drm_type` | `widevine` (default) or `playready`. |
| `mode` | `proxy` (default) or `server_cdm`. |
| `vault_keys` (response) | KIDs whose content key came from a server vault, not the CDM. The client proves such a content key with a decode before it trusts it; the field is absent when every content key came from the CDM. |
| Status | Error code | Meaning |
| --- | --- | --- |
@@ -1084,6 +1086,20 @@ Get the content keys for the DRM. The `mode` field selects one of two modes.
| `404` | `NO_CONTENT` | Server CDM produced no keys. |
| `502` | `SERVICE_ERROR` | The service aborted during licensing. |
### `POST /api/session/{session_id}/keys/bad`
Report a content key from `vault_keys` that did not decrypt the track. The server flags the pair in its local vaults, under the name of the vault that served it, and the next licence for that KID reaches the CDM. The client never learns the server vault names.
```json
{ "kid": "<kid_hex>", "key": "<key_hex>" }
```
| Status | Error code | Meaning |
| --- | --- | --- |
| `200` | - | The pair is flagged. |
| `400` | `INVALID_INPUT` | This remote session was not served that pair. |
| `404` | `SESSION_NOT_FOUND` | Unknown remote session. |
### `GET /api/session/{session_id}`
Return remote session metadata and validity.
+1
View File
@@ -197,6 +197,7 @@ request and response in full.
| `POST` | `/api/session/{session_id}/segments` | Per-track segment/download details. |
| `POST` | `/api/session/{session_id}/segment_filter` | Server-side HLS segment filter results for one track. |
| `POST` | `/api/session/{session_id}/license` | Proxy or server-side DRM licensing. |
| `POST` | `/api/session/{session_id}/keys/bad` | Report a server-vault content key that did not decrypt. |
| `GET` | `/api/session/{session_id}/logs` | Drain the service's server-side log output. |
| `GET` | `/api/session/{session_id}/prompt` | Poll for interactive auth status/prompt. |
| `POST` | `/api/session/{session_id}/prompt` | Submit an answer to an auth prompt. |
+12
View File
@@ -236,6 +236,17 @@ There are two ways DRM keys get resolved, chosen by the client's `server_cdm` fl
- Enable with `server_cdm: true` in the server entry.
- The server tells the client which DRM type it used
(`widevine` or `playready`).
- A content key the server took from its own vault is unproven. The response
lists its KID in `vault_keys`, the client decrypts and checks that the output
decodes, and a wrong pair goes back to `POST /api/session/{id}/keys/bad`. The
server flags the pair in every local vault it holds, records the name of the
vault that served it, and the next licence for that KID skips the pair and
reaches the CDM. A server with no local SQLite vault cannot store the flag. The
server accepts a report only for a pair it served to that remote session. The
client never learns the server vault names.
- A content key in the client's own vaults goes first, even when the server
already answered. The client proves it, and a server key waits as the next
candidate.
---
@@ -313,6 +324,7 @@ The server mounts all these routes, even in `--remote-only` mode. Paths use the
| `POST` | `/api/session/{id}/segments` | Resolve per-segment/track download descriptors |
| `POST` | `/api/session/{id}/segment_filter` | Unwanted HLS segment URIs for one track (ads, bumpers) |
| `POST` | `/api/session/{id}/license` | DRM licensing (proxy or server CDM) |
| `POST` | `/api/session/{id}/keys/bad` | Report a server-vault content key that did not decrypt |
| `GET` | `/api/session/{id}/logs` | Drain the service's server-side log output |
| `GET` | `/api/session/{id}/prompt` | Poll interactive auth status / pending prompt |
| `POST` | `/api/session/{id}/prompt` | Submit an answer to a pending prompt |