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

El problema: no hay sesión que iniciar

Cuando desaparece el servidor que guardaba quién estaba conectado, la identidad deja de ser una fila en una tabla y pasa a ser una clave privada que solo tú controlas.

⏱ 20 min

Todo lo que hemos construido en los cincuenta y tres niveles anteriores tiene un agujero deliberado en el centro. Sabemos guardar el dato en el cliente, sabemos hacerlo converger sin coordinación y sabemos moverlo entre pares sin pasar por un centro, pero en ningún momento hemos dicho quién tiene derecho a hacer nada de eso. La respuesta habitual —un servidor que valida credenciales, emite una sesión y consulta una tabla de permisos en cada petición— es exactamente el componente que la arquitectura local-first ha eliminado. No es que sea caro o lento: es que no está. Y su ausencia no deja un hueco que rellenar con la misma pieza más pequeña, sino que obliga a cambiar la pregunta. La identidad deja de ser un registro que alguien mantiene sobre ti y pasa a ser una capacidad criptográfica que posees. Esta lección disecciona qué hacía en realidad aquel servidor, qué ocupa su lugar y por qué el cambio no es una sustitución técnica sino un cambio de modelo con consecuencias que llegan hasta el diseño de producto.

🎯 Al terminar esta lección sabrás
  • Descomponer las cuatro funciones distintas que un servidor de sesiones prestaba a la vez y que aquí se separan.
  • Entender por qué una clave privada puede sustituir a la autenticación pero no a la autorización.
  • Distinguir el modelo de listas de control de acceso del modelo de capacidades certificadas.
  • Reconocer qué garantías se pierden al eliminar el punto central y cuáles son irreducibles.

Qué hacía en realidad el servidor de sesiones

El botón de iniciar sesión esconde cuatro trabajos que rara vez se piensan por separado, y separarlos es el primer paso para entender qué se rompe cuando el servidor desaparece. El primero es autenticar: comprobar que quien llama es quien dice ser, normalmente contrastando un secreto compartido contra un almacén. El segundo es emitir un testigo de sesión, un valor opaco que evita repetir el paso anterior en cada petición y que solo tiene significado porque el mismo servidor que lo emitió puede reconocerlo. El tercero es autorizar: mirar una tabla de permisos y decidir si esa identidad puede hacer esa operación sobre ese recurso. Y el cuarto, el más invisible, es arbitrar: ser el único lugar donde ese estado existe, de modo que cualquier cambio es inmediatamente visible para todos y no hay dos versiones de la verdad.

En una aplicación local-first, los cuatro se desmontan de forma desigual. El primero es sustituible sin drama: la criptografía asimétrica autentica mejor que cualquier secreto compartido, porque demuestra posesión de una clave sin revelarla. El segundo directamente desaparece, porque un testigo opaco solo funciona si existe quien lo recuerde. El tercero es el problema real de este nivel y ocupa las tres lecciones siguientes. Y el cuarto —el arbitraje— es sencillamente irrecuperable: si no hay punto único, no hay instante en que todos los participantes compartan la misma vista de los permisos, y eso ya no es un detalle de implementación sino una propiedad del universo en el que estamos trabajando.

ℹ️
Este nivel se apoya en una especificación concreta y verificable

Las ideas de identidad sin servidor son antiguas, pero conviene anclarlas en algo revisable en lugar de hablar en abstracto. Usaremos como referencia UCAN, siglas de User Controlled Authorization Network, que su propia especificación describe como un esquema de autorización distribuido, sin confianza previa, de origen en el usuario y explícitamente local-first. Proporciona capacidades verificables con clave pública, delegables y extensibles, y logra la verificabilidad pública mediante cadenas de certificados de enlace tardío cuyos principales se representan con identificadores descentralizados. Cuando esta guía afirme algo sobre el formato, sale de ahí; cuando entre en terreno no especificado, se dirá con esas palabras.

La clave privada pasa a ser el sujeto

El reemplazo del primer trabajo es directo y es la parte fácil. En vez de enviar un secreto que el servidor compara, se firma un mensaje con una clave privada que nunca sale del dispositivo, y cualquiera que conozca la clave pública correspondiente puede comprobar la firma sin preguntar a nadie. Esto no es nuevo ni exótico: los navegadores llevan años ofreciendo claves no extraíbles a través de la API de criptografía web, las plataformas modernas exponen claves de acceso respaldadas por hardware y los enclaves seguros son ya equipamiento de serie en el consumo masivo. La materia prima está disponible en el dispositivo del usuario y no requiere infraestructura nueva.

Lo que sí es nuevo es la consecuencia semántica. La clave deja de ser un mecanismo de autenticación y pasa a ser el nombre mismo del sujeto. En el modelo antiguo tú eras la fila número 4812 y una clave era un método para demostrar que podías actuar como esa fila; en el nuevo no hay fila, y la única identidad globalmente única que existe es la derivada de la clave pública. La especificación de UCAN lo formula así: el sujeto se comporta como un identificador único global, con la propiedad añadida de ser verificable por clave pública, y esa imposibilidad de falsificarlo es precisamente lo que evita colisiones maliciosas de espacio de nombres que llevarían al problema del diputado confuso.

// Autenticar deja de ser preguntar y pasa a ser comprobar
const par = await crypto.subtle.generateKey(
  { name: "Ed25519" },
  false,            // no extraible: la privada no sale del dispositivo
  ["sign", "verify"]
);

// Nadie custodia la identidad: se demuestra firmando
const firma = await crypto.subtle.sign("Ed25519", par.privateKey, cuerpo);
const valida = await crypto.subtle.verify("Ed25519", par.publicKey, firma, cuerpo);

De aquí sale una recomendación de la especificación que conviene interiorizar antes de escribir una línea de código, porque contradice el instinto de la mayoría: las claves privadas no deberían moverse de un contexto a otro. Lo recomendado es una clave distinta por dispositivo físico y por caso de uso, para reducir la superficie de fuga y limitar el radio de daño ante un compromiso. Esto suena a incomodidad hasta que se ve la frase que lo justifica y que resume el nivel entero: compartir autoridad sin compartir claves es exactamente lo que proporcionan las capacidades. Si necesitas que otro dispositivo pueda hacer algo, no le mandas tu clave; le delegas.

Listas de control de acceso frente a capacidades certificadas

El tercer trabajo, autorizar, es donde el cambio de modelo se vuelve profundo. El enfoque dominante desde Multics es la lista de control de acceso: el recurso mantiene una relación de quién puede hacer qué, y en cada petición se consulta. La especificación de UCAN usa una analogía difícil de mejorar. Una lista de control de acceso es un portero con una relación de invitados: te identificas, el portero busca tu nombre y decide. Funciona bien mientras haya un solo portero. Con muchos locales y muchos porteros hay que coordinar por adelantado, sincronizar las denegaciones y aceptar que la probabilidad de dejar entrar a la persona equivocada por retraso de sincronización o por coincidencia de nombres no es cero.

Las capacidades certificadas funcionan como entradas de cine. Nadie comprueba tu documento porque quién eres es irrelevante: si tienes una entrada emitida por el cine para la sala 3, entras en la sala 3. Si no puedes ir, se la das a un amigo y no hace falta coordinar nada con el cine por adelantado. El precio aparece exactamente en un punto, y es el que ocupará la última lección de este nivel: si el cine necesita anular entradas, necesita una forma de identificarlas unívocamente y de compartir esa información entre los porteros.

flowchart LR
subgraph ACL [modelo de lista de control de acceso]
  U1[cliente] --> S1[servidor de sesiones]
  S1 --> T1[tabla de permisos]
  T1 --> R1[recurso]
end
subgraph CAP [modelo de capacidad certificada]
  U2[cliente con certificado] --> R2[recurso que verifica en local]
end
style S1 fill:#f38ba8,color:#11111b
style T1 fill:#f38ba8,color:#11111b
style U2 fill:#a6e3a1,color:#11111b
style R2 fill:#a6e3a1,color:#11111b

Hay una tercera familia que conviene nombrar para descartarla, porque es la que un ingeniero con formación en sistemas distribuidos propondría primero. Las capacidades de objeto, tal como se practican en la tradición que arranca del lenguaje E, son más expresivas y más robustas que los certificados: referencias, estado encapsulado y reenvío por proxy. Pero exigen dos propiedades que aquí no podemos pagar, y la especificación es explícita al respecto: comportamiento seguro ante fallo y preservación de la localidad. Ese énfasis en la consistencia descarta la tolerancia a particiones. Un sistema local-first que asume particiones como estado normal no puede adoptar ese modelo, y por eso UCAN escoge el modelo de capacidad certificada emparentado con SPKI en su lugar.

⚠️
La inversión de control es el cambio estructural, no la criptografía

Es tentador leer todo esto como lo mismo pero firmado, y esa lectura pierde lo esencial. Lo que ocurre es una inversión de control: no existe un servidor de autorización intermedio entre el solicitante y el recurso, y el propietario del recurso es directamente el servidor del recurso. La relación se da la vuelta: el recurso cede parte o toda la autoridad sobre sí mismo a determinados agentes, en lugar de haber un tercero que administre la relación entre unos y otros. Las ventajas que la especificación enumera se siguen de ahí y no de las firmas: distribución total, peticiones autocontenidas sin intermediario, tolerancia a particiones, granularidad flexible y composicionalidad, entendida como que deja de haber diferencia entre recursos que viven juntos y recursos que viven separados.

Lo que esto rompe

Conviene cerrar con lo que se pierde, porque el resto del nivel será más útil si se lee sabiendo dónde están los bordes. Se pierde, en primer lugar, la enumeración: con una tabla central puedes preguntar quién tiene acceso a un recurso y obtener una respuesta completa; con certificados delegables no puedes, porque un delegado puede subdelegar a terceros sin avisar a su delegador. La especificación lo dice sin rodeos: UCAN no ofrece confinamiento, ya que eso exigiría que todos los procesos estuviesen en línea, de modo que es imposible garantizar el conocimiento de todas las subdelegaciones existentes.

Se pierde, en segundo lugar, la revocación inmediata y gratuita. Borrar una fila de una tabla surtía efecto en la siguiente petición; retirar un certificado ya emitido exige entregar un mensaje a quien verifica, y esa entrega es asíncrona por definición. Y se pierde, en tercer lugar, la recuperación por parte de un tercero: no hay nadie con autoridad para restablecer tu acceso, porque la única prueba de que eras tú era la clave que has perdido. Las tres pérdidas son el temario de la quinta lección.

🪪

Autenticar: mejora

Firmar con una clave no extraíble demuestra posesión sin revelar el secreto y no necesita almacén compartido ni interlocutor en línea.

🎟️

Autorizar: cambia de forma

La consulta a una tabla se sustituye por la verificación de una cadena de certificados que el propio solicitante aporta.

📴

Sesión: desaparece

Un testigo opaco solo tiene sentido si alguien lo recuerda; sin ese alguien, cada petición debe ser autocontenida y verificable por sí sola.

⚖️

Arbitrar: se pierde

Sin punto único no hay instante en que todos compartan la misma vista de los permisos, y eso no se recupera con ninguna técnica.

No has quitado el servidor de autorización: has cambiado el momento en que se decide

La lectura ingenua de este nivel es que se elimina un componente y se sustituye por criptografía. La lectura correcta es que se mueve una decisión en el tiempo, y esa reformulación explica de golpe todas las ventajas y todos los problemas que veremos después. En el modelo de lista, la decisión de si puedes hacer algo se toma en el instante en que lo intentas, con toda la información disponible: la tabla está actualizada, el administrador acaba de cambiarla, la revocación de hace un segundo ya cuenta. Esa inmediatez es exactamente lo que hace tan cómodas las listas, y también lo que las obliga a existir en un punto único que todos consulten. En el modelo de capacidad, la decisión se toma en el instante de la delegación, que puede ser semanas antes, y lo que ocurre en el momento del uso ya no es una decisión sino una comprobación: verificar que el certificado dice lo que dice, que las firmas encadenan y que el tiempo no se ha agotado. Comprobar es barato, es local, es idempotente y es memoizable; decidir no lo era. Ahí está toda la ganancia. Y ahí está también, con precisión quirúrgica, todo el coste: cualquier información que llegue después del momento de la delegación —que la relación de confianza se rompió, que la clave se filtró, que las condiciones de privacidad del recurso cambiaron— tiene que viajar por un canal separado hasta cada verificador, porque el certificado ya salió y no puede desdecirse a sí mismo. Fíjate en que este es el mismo eje que atraviesa el track entero: en el nivel de conflictos vimos que las estrategias se ordenan según en qué instante deciden y cuánta información hay disponible entonces; aquí el patrón reaparece idéntico sobre la autoridad en lugar de sobre el dato. La revocación es difícil por exactamente la misma razón por la que un CRDT no puede deshacer una operación ya propagada: en un sistema sin punto de coordinación, lo que ya se emitió es historia, y la historia solo se corrige añadiendo, nunca borrando. Cuando en la quinta lección veas que las revocaciones deben ser inmutables, irreversibles y tratarse como un conjunto que solo crece, no lo leas como una restricción arbitraria de la especificación: es la única forma que tiene la corrección de una decisión pasada de convivir con la entrega desordenada. La autorización, aquí, es un CRDT.

⚔️ Desmonta el inicio de sesión de tu aplicación
  1. Enumera cada punto de tu código donde se consulta la identidad del usuario y clasifícalo en autenticación, autorización o arbitraje.
  2. Para cada punto de autorización, anota qué información necesita y en qué instante estaría disponible si tuviera que decidirse por adelantado.
  3. Identifica al menos un permiso de tu modelo que cambie con frecuencia y estima cuánto tiempo podría tolerar quedar desactualizado.
  4. Genera un par de claves no extraíble con la API de criptografía del navegador y comprueba que la clave privada no puede exportarse.
  5. Escribe en una frase qué pasaría hoy en tu aplicación si el usuario perdiera el dispositivo, y guárdala para contrastarla al final del nivel.