Por qué funciona sin conexión: la verificación es local
Validar una cadena de delegación es comprobar firmas, atenuación y tiempo sobre datos que ya tienes delante, de modo que la tolerancia a particiones no es una característica añadida sino una consecuencia.
Todo lo construido hasta aquí converge en una única propiedad práctica, y es la que justifica haber pagado el precio de abandonar el modelo de tabla: para saber si una petición está autorizada no hace falta preguntarle a nadie. El verificador recibe una invocación acompañada de su cadena de pruebas, y con eso ya tiene todo lo necesario. Comprueba firmas con claves que extrae de los propios identificadores, comprueba que cada eslabón no amplía lo que recibió, comprueba que el intervalo de validez cubre el instante actual y comprueba que el último eslabón cierra contra una raíz donde el emisor cede sobre sí mismo. Ninguno de esos cuatro pasos toca la red. Esa es la razón por la que la especificación puede describir la delegación como una forma de pasar, atenuar y asegurar autoridad de manera tolerante a particiones, y por la que el modelo encaja con lo que llevamos cincuenta y tres niveles construyendo. Esta lección desarma el algoritmo pieza a pieza, muestra el almacén local que lo hace eficiente y delimita con honestidad los tres puntos donde la localidad se rompe.
- Escribir el algoritmo de validación completo y comprobar que ninguno de sus pasos requiere red.
- Relacionar la tolerancia a particiones de este modelo con los compromisos que estudiamos en el nivel de CAP.
- Usar el almacén de delegaciones y la memoización de validación sin introducir errores de caducidad.
- Identificar los tres puntos donde la verificación deja de ser puramente local y qué hacer en cada uno.
El algoritmo de validación cabe en la cabeza
La virtud del modelo es que el procedimiento completo es corto y no tiene ramas ocultas. Se recorre la cadena desde la invocación hacia la raíz comprobando cuatro invariantes en cada paso. Primero, que la firma del eslabón se verifica con la clave pública que se obtiene de decodificar el identificador que figura como emisor; con did:key esa obtención es una decodificación local, como vimos. Segundo, que el eslabón se apoya en el anterior de la única forma admisible: la audiencia del eslabón previo debe ser el emisor del actual, y el sujeto debe mantenerse. Tercero, que el comando invocado queda probado por el comando delegado bajo la comparación por segmentos. Y cuarto, que el instante de ejecución cae dentro del intervalo de validez.
// Recorrido de la cadena: cuatro invariantes, cero llamadas de red
async function validar(cadena, invocacion, ahora) {
for (let i = 0; i < cadena.length; i++) {
const paso = cadena[i];
const clave = resolverLocal(paso.iss); // decodificacion, no consulta
if (!(await verificarFirma(clave, paso))) return false;
const previo = cadena[i + 1];
if (previo && previo.aud !== paso.iss) return false; // el enlace es el unico admisible
if (previo && previo.sub !== paso.sub) return false;
if (!pruebaComando(paso.cmd, invocacion.cmd)) return false;
if (paso.nbf && ahora + MARGEN < paso.nbf) return false;
if (paso.exp !== null && ahora - MARGEN > paso.exp) return false;
}
const raiz = cadena[cadena.length - 1];
return raiz.iss === raiz.sub; // la cadena cierra sobre si misma
}
El cuarto punto merece un comentario porque es donde se cuela el mundo físico. Los límites temporales se expresan como segundos enteros desde el inicio de la época Unix en tiempo universal coordinado, y la especificación exige rechazar como inválidas las marcas fuera del rango representable con precisión en lenguajes de un solo tipo numérico. Más importante todavía: advierte que, por la deriva de los relojes, los límites temporales no deberían considerarse exactos, y recomienda un margen de sesenta segundos en ambos sentidos. Es la misma lección del nivel de relojes reaparecida en otro sitio: la hora del sistema no sirve para ordenar, pero aquí sí hace falta usarla, y por tanto hay que admitir que miente un poco.
El intervalo de validez de una cadena tampoco es el del certificado que tienes en la mano, sino el de la cadena entera: va desde el instante de inicio más tardío hasta la caducidad más temprana de toda la cadena de delegación. Un eslabón que caduca mañana anula en la práctica a otro que caduca dentro de un año, y esa propiedad es exactamente lo que permite usar caducidades cortas como mecanismo de control barato, tal como recomienda el principio de mínima autoridad.
La especificación distingue con cuidado varios instantes en la vida de un certificado: el de delegación, cuando alguien lo emite; el de invocación, cuando alguien lo usa; el de validación, que puede ocurrir varias veces; y el de ejecución, término que la especificación adopta para evitar la palabra sobrecargada tiempo de ejecución. Señala dos puntos naturales de validación —al recibir una delegación y al ejecutar una invocación— y establece que en el momento de la ejecución la validación es obligatoria. La consecuencia práctica es que validar al recibir es una optimización útil para rechazar pronto lo que no sirve, pero nunca sustituye a la comprobación del ejecutor.
Tolerancia a particiones por construcción
Conviene precisar por qué esta propiedad no es una característica que alguien decidió añadir, sino una consecuencia estructural de haber invertido el control. Un sistema de listas necesita que la lista esté disponible en el momento de decidir; si la red se parte, o bien se deniega el acceso o bien se sirve una copia posiblemente obsoleta, y ambas opciones son formas de decir que el sistema no tolera la partición. Aquí no hay nada que consultar: la prueba viaja con la petición y el verificador tiene todo lo que necesita.
La especificación es explícita sobre el contraste. Al comparar familias de sistemas de autorización, señala que las capacidades de objeto exigen comportamiento seguro ante fallo y preservación de la localidad, y que ese énfasis en la consistencia descarta la tolerancia a particiones. Frente a eso, entre las ventajas de la inversión de control enumera explícitamente la tolerancia a particiones y el soporte para datos y máquinas replicadas. En una nota al pie afina todavía más el encaje en la terminología clásica: se trata de un sistema del tipo que privilegia la consistencia ante una partición, con la observación de que el modelo puede expresar también la variante que privilegia la disponibilidad, aunque lo más típico sea privilegiar la consistencia durante la partición y la latencia el resto del tiempo.
flowchart TD P[peticion con cadena de pruebas] --> V[verificador local] V --> F[comprobar firmas con claves decodificadas] V --> A[comprobar atenuacion por segmentos] V --> T[comprobar intervalo de validez con margen] V --> R[comprobar cierre en la raiz] F --> D[decision] A --> D T --> D R --> D N[red] -.-> X[no participa] style N fill:#f38ba8,color:#11111b style X fill:#f38ba8,color:#11111b style D fill:#a6e3a1,color:#11111b
Hay un rincón del modelo que conviene conocer porque es el que lo conecta directamente con los CRDT del track. Normalmente el ejecutor de una invocación es el sujeto, es decir, quien controla el recurso. Pero cuando el recurso no tiene una única localización —una máquina de estados replicada, un tipo de dato convergente—, la especificación propone levantar el requisito de que el ejecutor sea el sujeto. Lo formula como una cuestión de empujar frente a tirar: al empujar, el sujeto debe ser el sitio concreto al que se empuja; al tirar, el invocador no exige que una réplica particular aplique su estado. Aplicar un cambio a una réplica local y mantener un registro de invocaciones es una actualización válida del tipo replicado, existe una versión del sujeto localmente aunque su clave privada no esté presente, y difundir esos cambios entre pares permite que cada uno aplique lo que va conociendo. La frase que cierra el argumento es la que importa: gracias a ese registro de invocaciones, la procedencia de la autoridad queda documentada de forma transparente.
El almacén local: memoizar, caducar, olvidar
Que la validación sea local no significa que deba repetirse entera cada vez. La especificación describe un almacén de delegaciones que el validador puede mantener con lo que ha ido recibiendo, indexado por identificador de contenido, sobre el que pueden construirse índices adicionales para acelerar la búsqueda o la selección de capacidades. Los certificados son inmutables pero están acotados en el tiempo, de modo que el almacén puede desalojar los caducados o revocados.
Encima de ese almacén se apoya la optimización que hace el sistema barato en régimen permanente. Al margen de la revocación, la validación de capacidades es idempotente: marcar un identificador como válido actúa como memoización y evita comprobar la estructura entera en cada validación. La especificación va un paso más allá y recomienda extender esa memoización a certificados distintos que comparten una prueba: si esa prueba ya se revisó y no está revocada, es recomendable considerarla válida de inmediato.
// La memoizacion es correcta porque validar es idempotente salvo revocacion
function validarConCache(cid, cadena, invocacion, ahora, cache) {
if (cache.invalidos.has(cid)) return false; // revocado: falla sin recorrer nada
if (cache.validos.has(cid)) return dentroDeVigencia(cache.validos.get(cid), ahora);
const ok = validar(cadena, invocacion, ahora);
if (ok) cache.validos.set(cid, intervaloDeValidez(cadena));
return ok;
}
// La revocacion es irreversible: propaga a todos los derivados y no se retira
function marcarRevocado(cid, cache) {
cache.invalidos.add(cid);
for (const derivado of cache.derivadosDe(cid)) cache.invalidos.add(derivado);
cache.validos.delete(cid);
}
La misma estructura sirve para el otro requisito obligatorio del modelo. La prevención de repetición es exigida por la especificación, y descansa en que todo certificado debe resumir a un identificador de contenido único; las estrategias simples que sugiere son mantener un conjunto de identificadores ya vistos o exigir que los valores de un solo uso crezcan de forma monótona por principal. Recomienda además mantener un índice secundario por caducidad, que habilita la recolección de basura y búsquedas más eficientes, y para almacenes muy grandes admite las técnicas habituales de caché: filtros de Bloom, cachés multinivel y demás. Es, otra vez, el mismo material que estudiamos al reconciliar réplicas puesto a trabajar sobre certificados.
Dónde deja de ser local
La honestidad exige señalar los tres puntos donde la palabra local deja de aplicarse limpiamente, porque cada uno es una fuente real de sorpresas en producción.
El primero es la disponibilidad de las pruebas. La cadena tiene que estar completa donde se valida, y la especificación deja la resolución de certificados al transporte, exigiendo únicamente que toda especificación de transporte defina protocolo de petición, protocolo de respuesta y formato de colecciones. Y añade una consecuencia dura que conviene grabar: si una instancia no puede desreferenciar un identificador de contenido en tiempo de ejecución, el certificado debe fallar la validación, en coherencia con la semántica constructiva del modelo. Ausencia de prueba no es duda: es rechazo.
El segundo es la validez semántica. Ya lo adelantamos: una cadena impecable puede autorizar algo sobre un recurso que el emisor raíz ya no controla, y el ejecutor debe verificar la propiedad de recursos externos en el momento de ejecutar. Esa verificación puede muy bien no ser local, y es el punto exacto por el que la especificación recomienda que el ejecutor sea el propio recurso siempre que sea posible.
El tercero es la revocación, que es no monótona y por tanto tiene que llegar por un canal aparte. Un verificador aislado sigue considerando válido lo que fue revocado hace una hora si nadie se lo ha contado, y eso no es un defecto de implementación sino la definición misma de trabajar sin conexión. Es el tema de la lección siguiente.
Cuatro comprobaciones
Firma, enlace entre eslabones, atenuación por segmentos e intervalo de validez con margen de un minuto por deriva de reloj.
Almacén indexado por contenido
Guardar lo recibido por identificador permite memoizar la validación y desalojar lo caducado o revocado sin ambigüedad.
Repetición prevenida
Todo certificado resume a un identificador único; el ejecutor lo contrasta contra el conjunto de resúmenes aún no caducados.
Prueba ausente es rechazo
Si no puedes desreferenciar un enlace de la cadena en tiempo de ejecución, la validación falla; la semántica es constructiva.
Merece la pena detenerse en lo que acaba de ocurrir, porque es más profundo que una ventaja de rendimiento y explica por qué este nivel está donde está dentro del track. Durante décadas, autorizar ha significado consultar, y consultar es una operación que presupone un interlocutor disponible. Esa presuposición estaba tan enterrada en el modelo que ni siquiera se enunciaba: por supuesto que la base de datos está ahí, por supuesto que el servidor de identidad responde. Lo que hace este diseño es tomarse en serio que esa suposición no se cumple, y al hacerlo descubre algo que no era evidente: en un sistema que admite particiones, la única forma de autorización que se puede definir con precisión es la que se decide con la información que ya está en el dispositivo. Cualquier otra cosa no es autorización, es una consulta que a veces funciona. Y el argumento es más fuerte de lo que parece, porque no depende de la tecnología ni de la especificación. Si tu verificador está partido de la red y aun así tiene que decidir, solo tiene tres opciones lógicamente posibles: denegar siempre, permitir siempre, o decidir con lo que ya tiene. Las dos primeras son inaceptables por razones opuestas y simétricas. La tercera es este modelo, y no hay una cuarta. Cuando lo ves así, la cadena de certificados deja de parecer una técnica ingeniosa y se revela como la única forma que puede adoptar la respuesta. Fíjate ahora en el paralelismo exacto con el resto del track, porque no es una analogía sino la misma estructura repetida. Los tipos de dato convergentes existen porque preguntamos qué operaciones sobre datos pueden aplicarse sin coordinación, y la respuesta fue: las que conmutan. Las cadenas de delegación existen porque preguntamos qué decisiones de autoridad pueden tomarse sin coordinación, y la respuesta fue: las que son monótonas y verificables localmente. En ambos casos partimos de una operación aparentemente irreducible —fusionar estados, conceder permisos— y encontramos el subconjunto de ella que sobrevive a la ausencia de un árbitro. En ambos casos, ese subconjunto resultó ser lo bastante grande para construir aplicaciones reales. Y en ambos casos, lo que queda fuera es exactamente lo mismo y no se puede rescatar: las operaciones que necesitan saber lo que nadie te ha contado todavía. Un CRDT no puede implementar una restricción de unicidad global porque necesitaría ver todas las réplicas; una cadena de certificados no puede saber que fue revocada porque necesitaría ver todos los mensajes. Son la misma imposibilidad enunciada dos veces. Por eso el diseño maduro de un sistema local-first no consiste en buscar técnicas más listas que la esquiven, sino en algo mucho menos glamuroso y mucho más eficaz: empujar la mayor cantidad posible de decisiones al subconjunto que sí sobrevive, y tratar lo que quede fuera como excepción rara, cara y explícita. Caducidades cortas en lugar de revocaciones frecuentes es exactamente esa filosofía aplicada a la autoridad, del mismo modo que elegir un tipo que conmuta en lugar de resolver conflictos lo era aplicada al dato.
- Implementa el validador completo de cuatro invariantes y ejecútalo con el adaptador de red deshabilitado a propósito.
- Añade el margen de sesenta segundos por deriva de reloj y construye un caso límite que solo pase gracias a él.
- Introduce una caché memoizada por identificador de contenido y mide cuántas verificaciones de firma te ahorras en una sesión típica.
- Añade el conjunto de identificadores ya invocados y demuestra que una invocación repetida es rechazada.
- Simula la ausencia de un eslabón intermedio y comprueba que tu validador rechaza en lugar de dudar o de conceder por omisión.