Confianza cero en el transporte: pedirle un bloque a cualquiera
Si el nombre verifica el contenido, la fuente deja de tener que ser digna de confianza: se puede aceptar un bloque de un desconocido o de un hostil, y eso libera por completo la topología de la red.
Las dos lecciones anteriores establecieron que un nombre por contenido permite comprobar lo que se recibe. Esta se ocupa de la consecuencia arquitectónica de esa comprobación, que es la más profunda de todo el nivel y la que explica por qué esta técnica aparece siempre que hay que distribuir datos sin una autoridad central. Si puedo verificar por mi cuenta que los bytes que me han dado son los que pedí, entonces la identidad de quien me los ha dado deja de formar parte del argumento de seguridad. Puedo pedírselos a un servidor de la empresa, a un ordenador desconocido de la otra punta del mundo, a un intermediario que los tenía guardados de otra petición o a alguien que quiere activamente perjudicarme, y en los cuatro casos la respuesta es igual de buena o se descarta igual de rápido. El canal deja de ser el lugar donde vive la confianza. Esta lección examina qué se libera exactamente con eso, qué arquitecturas habilita y, con el mismo cuidado, qué sigue sin resolver.
- Entender por qué la verificación en el receptor elimina el requisito de que la fuente sea de confianza.
- Separar las garantías que da el nombre de las que siguen dependiendo del canal y de los metadatos.
- Reconocer los patrones de red que esta propiedad habilita y por qué antes eran inviables.
- Delimitar con honestidad qué ataques siguen abiertos aunque toda la integridad esté cubierta.
Aceptar datos de una fuente que no merece confianza
El modelo de seguridad habitual del transporte funciona por autenticación del extremo: se establece un canal cifrado con un servidor cuya identidad se comprueba contra una cadena de certificados, y a partir de ahí se cree lo que ese servidor diga. Es un modelo sólido y bien entendido, y tiene una característica que rara vez se enuncia: la garantía es sobre el interlocutor, no sobre el dato. El canal asegura que hablas con quien crees, y de ahí se infiere, por confianza, que lo que te cuenta es correcto.
Con nombrado por contenido esa inferencia se vuelve innecesaria porque el dato se defiende solo. La secuencia es distinta y merece escribirse con precisión: se obtiene un nombre por algún medio en el que sí se confía, se pide ese nombre a quien sea, se recibe algo, se recalcula el resumen y se compara. Si coincide, los bytes son necesariamente los correctos, con independencia de la ruta, del intermediario y de las intenciones de quien respondió. Si no coincide, se descarta y se pregunta a otro, y lo único que el respondedor hostil ha conseguido es hacernos perder un poco de ancho de banda.
// La fuente es intercambiable porque la comprobacion la hace el receptor
async function obtener(nombre, fuentes) {
for (const fuente of fuentes) { // orden arbitrario, sin jerarquia
const bytes = await fuente.pedir(nombre);
if (await hash(bytes) === nombre) return bytes;
fuente.penalizar(); // mintio: baja su prioridad
}
throw new Error('nadie tiene ese bloque');
}
flowchart LR P[peticion por nombre] --> F1[servidor conocido] P --> F2[companero desconocido] P --> F3[intermediario hostil] F1 --> V[recalcular el resumen] F2 --> V F3 --> V V --> OK[coincide: aceptar] V --> KO[no coincide: descartar y seguir] style V fill:#cba6f7,color:#11111b style OK fill:#a6e3a1,color:#11111b style KO fill:#f38ba8,color:#11111b
Conviene apreciar la asimetría de esfuerzo que esto introduce, porque es lo que hace el esquema robusto en la práctica y no solo en el papel. Para el receptor, comprobar cuesta un recorrido de los bytes que ya iba a leer de todos modos. Para el atacante, fabricar un bloque que pase la comprobación exige romper la función resumen. No hay ninguna cantidad de nodos maliciosos que mejore esa situación: mil atacantes coordinados tienen exactamente la misma probabilidad de colar un bloque falso que uno solo, que es ninguna. Lo que sí pueden hacer los atacantes es negarse a responder, y ese es un problema distinto que la última sección aborda.
Esa insensibilidad al número de adversarios es lo que separa este modelo de casi todos los esquemas de confianza distribuida que se han intentado. Los sistemas que deciden por mayoría, por reputación o por votación degradan a medida que crece la fracción hostil, y por eso necesitan controlar la identidad de los participantes o encarecer su creación. Aquí no hay nada que votar: la comprobación es local, determinista y no consulta a nadie, de modo que la fracción de nodos honestos deja de ser un parámetro del modelo de seguridad y pasa a ser, como mucho, un parámetro de rendimiento. Es una diferencia de género y explica por qué estas redes pueden admitir a cualquiera sin registro previo.
Qué deja de importar y qué sigue importando
La formulación cómoda de esta propiedad —el transporte ya no importa— es falsa y produce sistemas malos, así que conviene desglosarla. Lo que deja de importar es la integridad de la carga útil. Todo lo demás sigue exactamente donde estaba, y algunas cosas incluso empeoran, porque un protocolo que reparte peticiones entre muchos desconocidos expone más superficie que uno que habla siempre con el mismo servidor.
Integridad: resuelta
Nadie puede alterar un byte sin que se note, ni un intermediario, ni un disco defectuoso, ni un adversario en el camino.
Confidencialidad: intacta
El bloque viaja tal cual. Si su contenido es sensible, hay que cifrarlo antes de nombrarlo, y entonces el nombre lo es del texto cifrado.
Metadatos: peor
Preguntar por un nombre revela a cada fuente consultada qué se está buscando, y eso es información valiosa para quien observa.
Disponibilidad: sin cambios
Nadie está obligado a responder. La verificación no ayuda contra quien simplemente calla, y esa es la vía de ataque que queda abierta.
La segunda tarjeta encierra un detalle de diseño que decide la arquitectura entera y que conviene fijar ahora. Si el contenido es sensible, el orden correcto es cifrar primero y nombrar después, de modo que el resumen identifique el texto cifrado. Así el bloque se puede replicar, cachear y servir por cualquiera sin revelar nada, y la clave viaja por un canal separado junto al nombre. El orden inverso —nombrar el texto claro y cifrar solo el transporte— arruina la propiedad, porque entonces cada nodo que almacena necesita ver el contenido y la deduplicación entre usuarios distintos se vuelve imposible sin filtrar información.
// Cifrar y despues nombrar: el bloque circula sin revelar nada
const clave = generarClave();
const cifrado = await cifrar(clave, bytes);
const nombre = await hash(cifrado); // el nombre lo es del texto cifrado
// se publica el nombre; la clave viaja aparte, con quien deba leerlo
La tercera tarjeta señala el coste menos discutido y merece el mismo cuidado. Un protocolo que reparte peticiones entre varias fuentes multiplica el número de observadores que aprenden qué se está buscando, y como el nombre es determinista, cualquiera que conozca de antemano un contenido puede reconocer que se está pidiendo. Es una regresión real frente a un canal único cifrado con un servidor que ya lo sabía todo. Las mitigaciones existen —pedir bloques en desorden, mezclar peticiones señuelo, encaminar a través de terceros—, todas cuestan tráfico o latencia, y ninguna es gratuita como sí lo es la verificación.
Lo que esto habilita en una red entre pares
Con la integridad resuelta en el receptor, un conjunto de patrones que en un modelo de confianza en la fuente serían impracticables pasan a ser triviales. Merece la pena repasarlos porque son, literalmente, el catálogo de arquitecturas de distribución que existen hoy.
El primero es la descarga desde múltiples orígenes simultáneos. Un fichero representado como árbol de bloques se puede pedir a diez pares distintos a la vez, un trozo a cada uno, y ensamblar el resultado sin que ninguno de ellos sepa nada de los demás ni haya que decidir cuál es el bueno. Cada trozo se valida por separado y el trozo que falla se vuelve a pedir a otro. Esta es la idea que sostiene los protocolos de intercambio de ficheros desde hace dos décadas y la que hace que la capacidad de servicio crezca con el número de participantes en lugar de saturarse.
// Descarga desde varias fuentes: cada trozo se valida por separado
async function traer(nombresDeBloque, fuentes) {
return Promise.all(nombresDeBloque.map((n, i) =>
obtener(n, rotar(fuentes, i)) // reparto por trozo, sin negociar nada
));
}
El segundo es la caché oportunista sin permiso. Cualquier nodo que haya visto pasar un bloque puede guardarlo y servirlo después, y quien lo reciba de él no necesita saber que ese nodo existe ni concederle ninguna autoridad. En un modelo por ubicación, un intermediario que sirve contenido en nombre de otro es un problema de seguridad que exige delegación explícita; aquí es simplemente un nodo más con el mismo bloque.
El tercero es el intercambio sin red. Dos dispositivos que se encuentran por proximidad, un disco que se transporta a mano, un fichero recibido por mensajería: todos son fuentes válidas, porque la validez no depende del medio. Para un track de local-first esto no es una curiosidad, es el escenario central: dos personas en el mismo edificio sin conexión al exterior pueden intercambiar el estado de su trabajo y comprobar que es correcto, sin que exista ningún servidor al que preguntar.
El cuarto es la reanudación y la reparación sin protocolo especial. Una transferencia interrumpida deja un conjunto de bloques ya verificados; reanudarla consiste en pedir los que faltan, a quien sea, sin negociar desplazamientos ni confiar en que el otro extremo recuerde la sesión. Y si un disco se corrompe años después, reparar el fichero es pedir de nuevo solo los bloques cuyo resumen ya no cuadra, no la copia entera.
# El mismo bloque, tres origenes, la misma comprobacion en el receptor
ipfs get bafybeigd... # de la red publica
ipfs get bafybeigd... --offline # de la cache local, si ya estaba
# o de un disco que alguien trajo en la mano: el nombre lo valida igual
En una arquitectura por ubicación, delegar la entrega de un dato exige un acuerdo: certificados, claves de firma, cláusulas de responsabilidad. Es la razón de que las redes de distribución sean empresas y no voluntarios. Cuando el nombre valida el contenido, servirlo no requiere que se confíe en el servidor para nada, y por tanto cualquiera puede participar sin pedir permiso ni asumir responsabilidad sobre la corrección. El coste de entrar en la red de distribución cae a cero.
Los límites: lo que el resumen nunca dirá
Cerrar sin el inventario de lo que sigue abierto sería deshonesto, y además esos límites son los que determinan qué hay que construir encima. Conviene enunciarlos ordenados por lo caro que resulta ignorarlos, porque en la mayoría de los diseños fallidos de esta familia el error no fue confiar en el transporte, sino creer que la verificación cubría alguna de estas cuatro cosas.
El primero es la disponibilidad. Un adversario que no puede alterar bloques sí puede negarse a servirlos, y si controla suficiente parte de la red puede lograr que un nombre concreto sea inencontrable. La defensa no es criptográfica sino de replicación e incentivos, y es un problema abierto.
Conviene precisar cómo se ataca la disponibilidad aquí, porque no es fuerza bruta sino aislamiento. Si un adversario consigue que todas las fuentes que un nodo conoce estén bajo su control, ese nodo no recibirá nunca los bloques que le interesan y tampoco sabrá que existen: la verificación le protege de recibir mentiras, no de no recibir nada. La defensa es de descubrimiento y de diversidad de rutas —conocer fuentes obtenidas por caminos independientes, incluidas las de la propia red local— y es un problema que se parece mucho más al enrutamiento que a la criptografía.
El segundo es la privacidad de la petición. Preguntar por un nombre es revelar interés por un contenido, y si el contenido es conocido públicamente, el nombre lo identifica sin ambigüedad. Un observador que ve pasar peticiones aprende qué documentos consulta cada quién, y un servicio que deduplica entre usuarios permite confirmar si un contenido sospechado ya existe. Ambas cosas son fugas reales que se han explotado en la práctica.
El tercero es la autorización, y es el que más sorprende a quien viene de arquitecturas con servidor. Que un bloque sea verificable no dice nada sobre si quien lo pide debía poder verlo; si el contenido es sensible, el control de acceso hay que ejercerlo antes de entregarlo o, mejor, sustituirlo por cifrado, porque en una red donde cualquiera replica, la única política de acceso que sobrevive a la copia es no poder descifrar.
El cuarto es la frescura, que la lección siguiente desarrolla entera. Un bloque antiguo es tan válido como uno reciente, de modo que una fuente puede servir a propósito una versión obsoleta y el receptor la aceptará sin objeción. Ninguna cantidad de verificación de integridad detecta eso, porque el bloque antiguo no es falso: es viejo. Responder a esa pregunta exige un mecanismo que diga cuál es la versión vigente, y ese mecanismo tiene que ser mutable, con todo lo que eso arrastra.
La cadena de confianza no desaparece: se desplaza entera al momento en que obtuviste el nombre. Si te lo dio una respuesta sin autenticar, un adversario que la controle te dará el nombre de un contenido que él eligió, y tú lo verificarás con éxito y quedarás perfectamente convencido de haber recibido lo correcto. Por eso el nombre raíz de cualquier estructura siempre viene firmado, publicado por un canal autenticado o fijado en el propio programa. Auditar un sistema de este tipo consiste, casi siempre, en seguir hacia atrás de dónde salió el primer nombre.
La lección transferible de esta parte del nivel no es que se pueda descargar de varias fuentes a la vez, que es un truco conocido, sino la reorganización que lo hace posible: mover la comprobación desde el canal hasta el dato convierte la forma de la red en una decisión puramente económica. Vale la pena ver por qué eso es tan poco habitual. En la mayoría de las arquitecturas, la topología y la seguridad están fusionadas: se decide que hay un servidor central porque es el único sitio donde se puede validar; se decide que los clientes no hablan entre sí porque no hay forma de que se crean; se decide que la caché la gestiona un proveedor concreto porque hay que confiarle la entrega. Cada una de esas decisiones se presenta como técnica y en realidad es una consecuencia de dónde se puso la comprobación. Al ponerla en el receptor, todas se sueltan a la vez, y entonces las preguntas de topología —cuántas réplicas, quién sirve a quién, dónde se cachea, si hay servidor o no— se pueden contestar por latencia, por coste y por disponibilidad, que es como deberían contestarse siempre. La seguridad deja de ser una restricción sobre el diagrama y pasa a ser una propiedad del formato. Esto sugiere una pregunta que conviene hacerle a cualquier sistema distribuido que se herede o se diseñe: de todas las restricciones de su arquitectura, cuáles existen porque el receptor no puede comprobar lo que recibe. Suele ser una fracción sorprendentemente grande, y suele estar presentada como si fuera inevitable. Y conviene aplicar el mismo rigor en sentido contrario: soltar la topología no soluciona ni la disponibilidad, ni la privacidad de las peticiones, ni la pregunta de de dónde salió el primer nombre. Un sistema es tan descentralizado como el más centralizado de sus puntos de partida, y en casi todos los que se anuncian como distribuidos ese punto es la raíz mutable de la que habla la lección siguiente.
- Monta dos procesos que intercambien bloques por nombre y haz que uno de ellos devuelva bytes alterados a propósito.
- Comprueba que el receptor lo detecta, mide cuánto le cuesta detectarlo y qué hace después con esa fuente.
- Trocea un fichero grande en bloques, pídelos a dos fuentes en paralelo y verifica cada bloque al llegar en lugar de al final.
- Cifra el contenido antes de nombrarlo y comprueba qué cambia en la deduplicación entre dos usuarios distintos.
- Anota qué aprende un observador que solo vea la lista de nombres que pides, sin ver ningún contenido.
- Traza en tu propio sistema de dónde sale el primer nombre de la cadena y qué le pasa si esa fuente miente.