Spec canónica de protocols/capability-token/SCT_SPEC.md, copiada sin reescribir.
protocols/capability-token/SCT_SPEC.mdPágina generada desde
protocols/capability-token/SCT_SPEC.md. No se edita a mano:bun run docs:genla regenera ybun run docs:checkfalla si difiere.
1 (byte 0 del token).UnsupportedVersion); una v2 será un formato nuevo, no una
extensión. Durante una transición el emisor firma la versión que el daemon desplegado
acepta; no hay negociación.0; otro valor es Malformed.Big-endian, 204 bytes. Los 140 primeros son el cuerpo firmado.
| Offset | Bytes | Campo | Contenido |
|---|---|---|---|
| 0 | 1 | version | 1 |
| 1 | 1 | scope | 1 read · 2 publish · 3 ingest · 4 control |
| 2 | 1 | range_kind | 0 ninguno · 1 bytes · 2 grupos MoQT |
| 3 | 1 | flags | 0 (reservado) |
| 4 | 8 | kid | sha256(clave pública Ed25519 cruda)[0..8] |
| 12 | 16 | aud | sha256("styx.sct.aud\0" ‖ node_id)[0..16] |
| 28 | 16 | sid | los 16 bytes del sess_, pkg_ o ing_<32 hex> |
| 44 | 16 | actor | sha256("styx.sct.actor\0" ‖ actor_id)[0..16] |
| 60 | 32 | resource | digest del recurso (abajo) |
| 92 | 8 | range_start | 0 si range_kind = 0 |
| 100 | 8 | range_end | [start, end), start < end si hay rango |
| 108 | 8 | nbf | segundos Unix |
| 116 | 8 | exp | segundos Unix, exp > nbf |
| 124 | 16 | jti | aleatorio, único por token |
| 140 | 64 | signature | Ed25519 sobre "styx-sct/v1\0" ‖ bytes[0..140] |
Transporte en texto: base64url sin padding, 272 caracteres exactos (204 es múltiplo de 3, no hay bits sobrantes que un codificador no canónico pueda usar).
Cada tipo tiene su dominio; las tuplas se codifican u32 BE count ‖ (u32 BE len ‖ bytes)*,
así dos recursos distintos nunca comparten digest por concatenación (["a/b"] ≠ ["a","b"]).
| Recurso | Digest |
|---|---|
| asset (WT/H3) | sha256("styx.sct.res.asset\0" ‖ asset_id) |
| namespace MoQT | sha256("styx.sct.res.moqt-ns\0" ‖ tuple(ns)) |
| pista MoQT | sha256("styx.sct.res.moqt-track\0" ‖ tuple(ns) ‖ u32 len ‖ name) |
| raíz de ingesta (dec-0121) | sha256("styx.sct.res.ingest\0" ‖ root_id) |
| Transporte | Lugar |
|---|---|
| WebTransport (CONNECT) | query cap= de /styx/<sess_id> (el navegador no deja poner cabeceras) |
| HTTP/3 | query cap= o Authorization: Bearer <sct> (los dos a la vez: rechazo) |
| HTTP/3 empaquetado | sólo Authorization: Bearer <sct> en /styx/pkg/<pkg_id>/... (cada playlist, init y segmento; el reproductor la pone en cada petición) |
| Ingesta (conduit) | Authorization: Bearer <sct> en cada petición del wire de conduit al IngestSink (protocols/session-ipc/messages/ingest.md) |
| MoQT | SETUP del cliente, opción AUTHORIZATION TOKEN (0x03): Alias Type = USE_VALUE (0x3), Token Type = 0x53435401, Token Value = los 204 bytes crudos |
El daemon no anuncia caché de tokens MoQT: REGISTER → AUTH_TOKEN_CACHE_OVERFLOW,
USE_ALIAS/DELETE → UNKNOWN_AUTH_TOKEN_ALIAS.
En este orden; la primera que falla decide el error y el audit:
Malformed, UnsupportedVersion).NoKeysConfigured) y el kid es de una de ellas (UnknownKey).
El daemon acepta a la vez la actual y la siguiente (rotación).BadSignature).aud = el del daemon (WrongAudience).nbf ≤ now + 30 (NotYetValid),
now < exp + 30 (Expired).exp - nbf ≤ 600 s para read/control, ≤ 300 s para publish/ingest (LifetimeTooLong).sid existe, la abrió playback-svc por IPC, y lo hizo con este mismo actor y
resource (UnknownSession, SessionMismatch).jti no se ha usado (Replayed). La caché de jti está acotada y
pre-reservada; llena de tokens vivos, rechaza (ReplayCacheFull) en vez de olvidar.Cada petición posterior bajo un token admitido se re-autoriza: caducidad, sesión todavía
abierta y ligada al mismo actor y resource (cerrar la sesión por IPC revoca), scope
compatible con la operación, recurso de la petición y rango (bytes para lecturas WT/H3, grupos
para FETCH; un token con rango de grupos no cubre un SUBSCRIBE, que es abierto). En WT, una
lectura abierta (end = 0, "hasta EOF") o con end más allá del fichero se autoriza con el
fin real del asset, el mismo que se sirve. En H3, la sesión del path se comprueba contra el
token nada más admitirlo, antes de mirar la sesión, registrar la petición o resolver el Range.
Alcance de la revocación y la caducidad sobre lo que ya está abierto:
EXPIRED_AUTH_TOKEN o UNAUTHORIZED. Cada chunk de
un stream de publisher se re-autoriza, así que un stream abierto antes no inyecta nada
después.IngestSink admite el token (consume el jti); las
siguientes con el mismo token se re-autorizan en cada petición (caducidad, sesión ing_* aún
ligada, scope, recurso). El rango bytes es el tope [0, max) del fichero, no una región: un
token ingest sin él, o con inicio distinto de 0, se rechaza (RangeDenied). Acota un fichero,
no lo que un actor reserva: eso lo acotan los topes del daemon (fichero y staging por actor,
dec-0121 §2). Reanudar tras caducar pide un token nuevo para la misma sesión.
cmd.media.closeIngest por IPC revoca y aborta las subidas de la sesión.read (SEC-Z13): un token vale como mucho 600 s (+30 s de tolerancia) y
una reproducción larga lo renueva sin que el player lo note. playback-svc firma uno nuevo del
mismo sid, actor y recurso en POST /sessions/:sessionId/token con el vigente en el cuerpo
como prueba de posesión (firma, audiencia, exp + skew, sesión y actor de la entrega); sólo
lo pide el dueño de la entrega. Sólo renueva el último token emitido o el anterior (el
anterior cubre una respuesta perdida): uno de hace dos renovaciones no sirve. Cupo de 4
renovaciones por entrega con reposición de una por minuto, y edad máxima de 12 h (410): pasada
ésta, el cliente abre una sesión nueva. Cada renovación es una emisión (sct_issued) y cada
rechazo va al audit sin el token. El daemon no se entera de la renovación y no guarda nada de
ella: sigue verificando cada petición, así que una entrega cerrada o revocada rechaza también
el token renovado.
/styx/pkg: cada petición lleva su token; el player cambia el Authorization o
el cap= de las siguientes. El token anterior vale hasta su exp + skew.exp + skew. Para no
cortarla, el cliente abre un stream bidi y envía {"renew":"<SCT base64url>"}. El daemon
verifica el token como el del CONNECT (cuenta Ed25519 de la conexión, un rechazo es un
strike y el límite de rechazos cierra la conexión), exige que sea de la misma sesión, actor
y recurso y que viva más que el grant actual, y sustituye el grant (el barrido y el lease de
admisión lo siguen). La respuesta es el FIN del stream (renovado) o su reset 0x04 (el
grant no cambia). Una sesión que el barrido ya revocó no se resucita.pkg_*): si nadie pide nada durante STYX_MEDIA_PACKAGING_IDLE_S
(30..600 s, defecto 600: nunca más de lo que vive un token read) el daemon la cierra, libera
su plaza de STYX_MEDIA_PACKAGING_MAX y la desliga: el mismo token recibe 403 desde entonces.
Una petición en vuelo o un fetch que la retiene no cuentan como inactividad.Authorization ni cap=) no llega a la authority, así que el
daemon la audita él: línea security_audit con reason=missing_credential op=<read|connect> y
evento evt.security.capabilityRejected con motivo MissingCredential, sin ruta, cabeceras ni
cuerpo del cliente.| Transporte | Rechazo |
|---|---|
| MoQT | cierre de la sesión: UNAUTHORIZED (0x2), EXPIRED_AUTH_TOKEN (0x18), MALFORMED_AUTH_TOKEN (0x16). Cualquier request antes de un SETUP admitido cierra con UNAUTHORIZED |
| WebTransport | la sesión se cierra con código 403 y motivo unauthorized; una lectura fuera del token se resetea con 0x04 sin enviar bytes |
| HTTP/3 | 403 forbidden genérico, antes de mirar si la sesión existe (no enumera sesiones) |
STYX_SCT_SIGNING_KEY, semilla de 32 bytes).
El daemon tiene claves públicas (STYX_SCT_PUBLIC_KEYS, hasta 4).cap= se redacta en los logs del daemon. El audit lleva
motivo, operación, scope, sid y jti en hex tal cual (identificadores, no credenciales:
sin la firma no abren nada) y el actor sólo como su digest de 16 bytes.vectors.json: clave de prueba, reloj, sesión ligada, digests y ~20 tokens con su veredicto
esperado (válidos, firma alterada, cuerpo alterado, caducado, aún no válido, otra audiencia,
kid desconocido, otro firmante con el mismo kid, sesión no ligada, otro recurso, otro
actor, vida excesiva, versión, flags y truncado).