wandres.dev
IDENTIDAD SIN SERVIDOR · DID y delegación

Los problemas duros: revocar, recuperar, añadir un dispositivo

Las tres cosas que un servidor de sesiones resolvía gratis y que aquí cuestan caro, con lo que la especificación resuelve, lo que deja abierto y lo que hay que decidir a mano.

⏱ 23 min

Llegamos al punto donde el modelo enseña la factura. Las cuatro lecciones anteriores construyeron un sistema de autorización que funciona sin conexión, escala sin coordinación y no depende de que ninguna empresa siga existiendo. Ahora hay que mirar lo que se dejó por el camino, y no son detalles: retirar un permiso ya concedido, recuperar el acceso cuando la clave se pierde y hacer que un segundo dispositivo sea tratado como el mismo usuario. Un servidor de sesiones resolvía las tres con una consulta de actualización, un correo de restablecimiento y un inicio de sesión, y las resolvía tan bien que dejaron de percibirse como problemas. Aquí no hay ninguna de esas tres piezas, y conviene ver con exactitud cuánto de cada problema está resuelto por la especificación, cuánto está deliberadamente fuera de ella y cuánto es una decisión de producto disfrazada de decisión técnica. Esta lección no ofrece un remedio que lo arregle todo, porque no existe; ofrece el mapa preciso de dónde duele y qué compensaciones hay disponibles.

🎯 Al terminar esta lección sabrás
  • Aplicar el modelo de revocación como conjunto monótono y entender por qué debe ser inmutable e irreversible.
  • Reconocer que revocar una prueba no garantiza retirar el acceso y saber calcular el efecto real sobre una red de delegaciones.
  • Distinguir lo que la especificación resuelve sobre recuperación de cuenta de lo que deja explícitamente abierto.
  • Diseñar el enlace de un dispositivo nuevo y valorar el coste de delegar la capacidad total.

Revocar: el conjunto que solo crece

La especificación de revocación empieza situando el mecanismo en su lugar, y ese encuadre es lo primero que hay que interiorizar. La revocación es un método manual para revertir una delegación, que no puede deshacer mutaciones irreversibles —el ejemplo que da es haber enviado un correo— pero sí puede limitar el mal uso a partir de ese momento. Debe considerarse la última línea de defensa contra el abuso, y hay que preferir la caducidad proactiva mediante límites temporales u otras restricciones, porque esa vía no exige aprender más información de la que estaría disponible en un ordenador desconectado. Esa frase es la tesis entera del asunto.

Como los enfoques que fallan hacia el lado seguro no encajan con un diseño local-first, tolerante a particiones, cacheable y de baja latencia, la revocación se consigue por entrega de un mensaje infalsificable procedente de un delegador anterior. Funciona de forma parecida a una lista de bloqueo: identifica rutas de delegación retiradas y ya no aptas para su uso. Y sobre esa entrega la especificación impone una restricción severa que sorprende a quien viene de sistemas centralizados: las revocaciones deben ser inmutables e irreversibles, y quien las recibe debería tratarlas como un conjunto que solo crece. Si una revocación se emitió por error, no puede retirarse; lo que procede es emitir una nueva delegación única, por ejemplo cambiando el valor de un solo uso o los límites temporales. La razón que da es puramente operativa: eso evita confusión mientras la revocación se propaga por la red y hace que el almacén sea de solo añadir, y por tanto muy apto para caché y difusión entre pares.

{
  "iss": "did:web:alice.example.com",
  "aud": "did:web:zelda.example.com",
  "can": "ucan/revoke",
  "args": {
    "revoke": { "/": "bafkreiem4on23qnu2nn2jg7vwzxkns6sxi5faysq7ekwtjhugqga3vbhim" }
  }
}

El alcance de quién puede revocar es más amplio de lo que se supone. El emisor de una delegación concreta dentro de una cadena de pruebas puede revocarla, y eso no siempre coincide con revocar la delegación que uno mismo emitió: cualquier certificado que contenga una prueba donde el revocador figure como emisor —incluso transitivamente en la cadena— puede ser revocado. La autoridad para revocar puede además delegarse a un principal que no esté en la cadena, con un comando reservado del espacio de nombres ucan/revoke y un argumento que apunta por identificador de contenido a la delegación afectada.

⚠️
Revocar una prueba no garantiza retirar el acceso, y esto se calcula mal casi siempre

La especificación lo advierte con una analogía que no admite discusión: si alguien tiene dos entradas para una película y una queda invalidada por su número de serie, sigue pudiendo entrar presentando la otra. La revocación de una prueba concreta no garantiza que el agente pierda la capacidad; si puede construir una cadena válida que no dependa de la prueba revocada, conserva el acceso. En una red de delegaciones con varias rutas —y las hay en cuanto hay más de tres participantes— revocar un eslabón puede no cambiar nada. Lo que hay que calcular no es qué eslabón cortar, sino qué conjunto de eslabones deja sin ninguna ruta al agente para la capacidad concreta que quieres retirar. Y ese cálculo requiere conocer todas las rutas, que es justo lo que el sistema no puede garantizarte porque no ofrece confinamiento.

La mecánica de aplicación en el verificador es simple y encaja con el almacén de la lección anterior. El agente que controla un recurso debe mantener una caché de revocaciones de las que es sujeto, y puede además cachear revocaciones difundidas sobre otros sujetos como mecanismo de almacenar y reenviar. Durante la validación, el identificador canónico de cada delegación debe contrastarse contra esa caché, y si hay coincidencia la delegación afectada debe ignorarse, teniendo presente que eso puede no invalidar la cadena entera. Las revocaciones pueden desalojarse una vez el certificado al que se refieren caduca por sus propios mecanismos.

flowchart LR
A[Alice raiz] --> B[Bob]
B --> C[Carol]
B --> D[Dan]
C --> E[Erin]
D --> E
R[revocacion de Carol a Erin] -.-> C
N[Erin conserva acceso por la ruta de Dan]
style R fill:#f38ba8,color:#11111b
style N fill:#f9e2af,color:#11111b

Dónde vive ese almacén depende de la forma del recurso, y aquí la especificación conecta directamente con el resto del track. Para recursos con una única fuente de verdad, lo indicado es mantener el almacén en la misma localización física que el recurso, por ejemplo un extremo del servidor que liste los certificados revocados por identificador canónico. Para estructuras de datos de consistencia eventual, puede conseguirse incluyendo el almacén dentro del propio recurso: el ejemplo que da es un sistema de ficheros basado en tipos convergentes que mantenga el almacén de revocaciones en una ruta bien conocida. La revocación pasa entonces a sincronizarse por el mismo canal que los datos, que es exactamente la propiedad que hace el modelo utilizable en una aplicación real.

Recuperar: lo que la especificación no resuelve

Aquí conviene ser directo, porque es el punto donde más literatura optimista circula. La especificación de UCAN no define un mecanismo de recuperación de cuenta. No es un olvido: recuperar significa que alguien que ya no posee la clave vuelva a tener autoridad, y no hay ninguna operación criptográfica que produzca eso a partir de la nada. Cualquier recuperación posible consiste en que exista de antemano otra autoridad capaz de emitir una delegación nueva, y decidir quién es esa autoridad es una decisión de diseño de producto con consecuencias de las que el estándar no puede hacerse cargo.

Las formas que puede tomar esa autoridad previa son pocas y todas tienen un coste evidente. Puede ser otro dispositivo tuyo, y entonces perder el único dispositivo es perder la cuenta. Puede ser un conjunto de contactos que en su conjunto puedan emitir la delegación de recuperación, lo que traslada el problema a la disponibilidad y honestidad de esas personas. Puede ser una clave guardada fuera de línea, con el problema clásico de custodiar algo que casi nunca se usa. O puede ser un servicio que conserve una capacidad de emergencia, que funciona muy bien y reintroduce exactamente el punto central que la arquitectura había eliminado.

💡
La recuperación es un problema de producto y hay precedentes que conviene mirar

La propia especificación, al repasar trabajos relacionados, cita un sistema de autorización local-first que no se basa en certificados sino en un tipo de dato convergente para construir la lista de miembros, dispositivos y roles de un grupo, y destaca de él dos cosas: que tiene un mecanismo de invitación amistoso basado en un intercambio de testigo, y que resulta sencillo ver qué usuarios tienen acceso a qué, evitando el problema de confinamiento propio de muchos sistemas de autorización descentralizados. Esa observación es una pista valiosa: donde los certificados delegables pierden —enumerar quién tiene acceso, invitar a alguien de forma cómoda— hay diseños alternativos que ganan, y nada impide combinar ambos enfoques en un mismo producto.

Sí hay una pieza del rompecabezas que la especificación resuelve, y es la que evita que rotar una clave equivalga a perderlo todo. Al describir la capacidad total menciona que quien la recibe puede actualizar el documento mutable del sujeto, lo que incluye cambiar sus claves. Eso implica que, si el sujeto se identifica con un método cuyo documento puede cambiar, es posible mantener un nombre estable mientras el material criptográfico rota debajo. El coste es el que anticipamos en la segunda lección: un documento mutable vive en algún sitio consultable, y ese sitio es una dependencia de red que rompe la resolución puramente local. Es un compromiso legítimo y hay que tomarlo con los ojos abiertos.

Añadir un dispositivo: enlazar agentes

Este es el problema mejor resuelto de los tres, y la especificación lo nombra explícitamente. La capacidad total, el comando /, es útil para enlazar agentes: delegar todo el acceso a otro dispositivo controlado por el mismo usuario que debe comportarse como el mismo agente. La mecánica es directa. El dispositivo nuevo genera su propio par de claves, que nunca sale de él, y produce su identificador. El dispositivo antiguo emite una delegación a favor de ese identificador. A partir de ahí, el nuevo puede actuar y puede a su vez subdelegar. En ningún momento se ha copiado una clave privada, que es exactamente la disciplina que la especificación recomienda: claves únicas por dispositivo físico y por caso de uso, para reducir la oportunidad de fuga y limitar el radio de daño ante un compromiso. Compartir autoridad sin compartir claves.

// El dispositivo nuevo genera su identidad y solo transporta la parte publica
const par = await crypto.subtle.generateKey({ name: "Ed25519" }, false, ["sign", "verify"]);
const didNuevo = await construirDidKey(par.publicKey);

// El dispositivo antiguo emite la delegacion. Estrechar es casi siempre lo correcto
const enlace = await firmarDelegacion(clavePrivadaAntigua, {
  iss: didAntiguo,
  aud: didNuevo,
  sub: didCuenta,
  cmd: "/crud",              // en lugar de "/", que concede absolutamente todo
  exp: ahora + 60 * 60 * 24 * 90
});

La tentación de usar / en ese enlace es fuerte y conviene resistirla salvo que se entienda bien lo que concede. La especificación describe la capacidad total como algo semejante a un permiso de superusuario y enumera lo que habilita: actualizar el documento mutable del sujeto, es decir, cambiar sus claves; revocar delegaciones; y usar cualquier recurso que terceros hayan delegado al sujeto. Es extremadamente poderosa y debe usarse con cuidado, dice el texto. Un dispositivo secundario que solo necesita leer y escribir documentos no necesita poder reescribir la identidad de su dueño.

Queda un detalle de orden que en la práctica causa fallos y que la especificación anticipa. La entrega desordenada es habitual en sistemas distribuidos, y además un usuario malicioso puede retrasar deliberadamente revelar que tiene una capacidad hasta el último momento para esquivar la detección. Por eso se recomienda aceptar revocaciones sobre recursos que el agente controla incluso antes de haber recibido la delegación a la que apuntan. Un almacén que rechace lo que todavía no entiende crea justamente la ventana que un atacante busca.

🚫

Revocar: entrega asíncrona

Mensaje infalsificable, conjunto que solo crece, aplicado en la caché del verificador y desalojable cuando el certificado caduca.

🧭

Revocar: efecto parcial

Cortar una prueba no retira el acceso si existe otra ruta válida; hay que razonar sobre el conjunto de rutas, no sobre el eslabón.

🆘

Recuperar: fuera del estándar

No hay mecanismo definido; toda recuperación exige una autoridad previa acordada, y elegirla es una decisión de producto.

📱

Enlazar: resuelto

El dispositivo nuevo genera sus propias claves y recibe una delegación; nunca se copia una clave privada entre contextos.

Las tres cosas que un servidor daba gratis eran la misma cosa, y no la echabas de menos porque no tenía nombre

Vale la pena cerrar el nivel viendo por qué estos tres problemas aparecen juntos, porque no es casualidad ni mala suerte: son el mismo problema con tres caras, y el servidor de sesiones no lo resolvía tres veces sino una sola. Lo que aquel servidor tenía y aquí no existe es un punto donde el estado de la autoridad es verdad en el mismo instante para todos. Con eso, revocar es escribir una fila y que la siguiente petición ya lo vea. Recuperar es que un administrador reasigne la fila, porque la identidad era la fila y no la clave. Y añadir un dispositivo es no hacer nada, porque el dispositivo nunca fue nada: era una conexión hacia el sitio donde estabas tú. Las tres operaciones eran gratis por la misma razón, y esa razón —la existencia de un ahora compartido— es exactamente lo que la arquitectura local-first sacrifica de forma deliberada y consciente para poder funcionar sin conexión. Nadie te vendió ese sacrificio con esas palabras, y por eso duele después. Ahora bien, la conclusión útil no es lamentarlo, sino ver qué queda en su lugar, porque lo que queda tiene una estructura reconocible. Sin un ahora compartido, lo único que puede sostener la autoridad es la acumulación monótona de hechos firmados: delegaciones que solo estrechan y revocaciones que solo se añaden. Las dos son monótonas, las dos se pueden difundir entre pares sin coordinación, las dos convergen aunque lleguen desordenadas y ninguna necesita que nadie esté en línea. Si esa descripción te suena exactamente a un tipo de dato convergente, es porque lo es, y no en sentido figurado: la especificación misma sugiere alojar el almacén de revocaciones dentro de una estructura convergente, en una ruta bien conocida, para que se sincronice por el mismo canal que los datos. El nivel entero, visto desde arriba, no es un capítulo aparte sobre seguridad: es la comprobación de que el aparato conceptual del track —convergencia sin coordinación, direccionamiento por contenido, decisiones empujadas al momento donde hay información suficiente— es lo bastante general para gobernar también quién puede hacer qué. Y de ahí sale la regla práctica con la que conviene salir de aquí, que no es criptográfica sino de diseño y funciona incluso si mañana cambias de especificación: si tu producto necesita que una retirada de acceso surta efecto en un plazo garantizado, esa parte concreta necesita un punto de coordinación y no hay forma de esquivarlo. La respuesta madura no es fingir que la revocación resuelve eso, ni abandonar el modelo entero por un caso. Es partir el sistema por esa línea: la inmensa mayoría de las capacidades viven en el mundo monótono, con caducidades cortas y renovación silenciosa mientras haya conexión, y el puñado que exige garantías duras se declara explícitamente como dependiente de un servicio en línea, con su degradación documentada y visible para el usuario. Eso no es una derrota de la arquitectura local-first: es el mismo reconocimiento honesto que hicimos en el quinto nivel del track al enumerar cuándo local-first es la respuesta equivocada, aplicado ahora con precisión de bisturí a un solo campo de tu modelo en vez de a la aplicación entera.

⚔️ Rompe tu propio modelo de autoridad
  1. Dibuja la red de delegaciones de tu aplicación con al menos dos rutas hacia el mismo agente y calcula qué conjunto de revocaciones retira de verdad una capacidad.
  2. Implementa el almacén de revocaciones como conjunto que solo crece y comprueba que aceptar una revocación antes que su delegación no rompe nada.
  3. Coloca ese almacén dentro de la estructura convergente que ya sincronizas y mide cuánto tarda una revocación en llegar a un par que estaba desconectado.
  4. Escribe el flujo de enlace de un dispositivo nuevo sin transportar nunca una clave privada, y justifica por escrito el comando que delegas en lugar de usar la capacidad total.
  5. Elige tu mecanismo de recuperación, escribe quién es la autoridad previa y qué pasa si esa autoridad falla o miente.
  6. Recupera la frase que escribiste en la primera lección sobre perder el dispositivo y contrástala con lo que tu diseño hace ahora.