DEV Community

Las tres vidas de mi clave de firma JWT

Una sola clave privada firma todos los tokens que emite nuestro servidor de autenticación. Piérdela y un atacante acuña un token válido para cualquier usuario, sin necesidad de contraseña, y tus registros muestran un inicio de sesión impecable. Es el secreto más valioso de todo el sistema y, a lo largo de su vida, se ha mudado dos veces: nació dentro del proceso de la aplicación, luego fue desterrada a una bóveda de la que ya no puede salir y después se reencarnó en un tipo de clave completamente distinto.

Cada mudanza ocurrió en un emisor en producción, con tokens ya en circulación y validadores que cachean un estado que no controlamos. Esto es lo que realmente cuestan esas mudanzas, y la que más nos enseñó fue la que no anunciamos.

Vida uno: la clave que el servidor guardaba en el bolsillo

Al principio, la clave vivía donde se usaba. El servidor generaba una clave RSA-2048, la mantenía en memoria y firmaba sus tokens con RS256. Este es el comportamiento por defecto en casi todas partes, y funciona bien hasta el momento en que pronuncias en voz alta la palabra custodia.

La clave privada era accesible para cualquier cosa capaz de leer la memoria del proceso, su configuración o una copia de seguridad de cualquiera de las dos. Queríamos poder decirles a los clientes que una base de datos filtrada no expone nada útil, y una clave de firma que estaba a un solo proceso de distancia de esa base de datos convertía esa afirmación en una mentira. La clave tenía que salir del edificio.

Vida dos: la clave que se mudó a una bóveda de la que no puede salir

Introdujimos una costura -un ISignatureProvider al que el servidor llama cada vez que necesita una firma- y pusimos un KMS detrás. Ahora la clave se genera dentro del motor transit de Vault y nunca se exporta. La aplicación no la posee; envía a la bóveda los bytes que hay que firmar y recibe de vuelta una firma. Puede usar la clave, pero no puede exfiltrarla. Esa distinción es todo el quid del asunto.

Un volcado de memoria, un archivo de configuración derramado, una copia de seguridad filtrada: ninguno de ellos contiene ya la clave, porque la clave nunca estuvo en ninguno de esos lugares. Comprometer la aplicación ahora le compra al atacante la capacidad de pedirle a la bóveda que firme, algo que podemos limitar por tasa, auditar y revocar. Ya no le compra la clave en sí, que era algo sobre lo que no podíamos hacer nada una vez que desaparecía.

Aquí viene la parte que no incluimos en las notas de la versión. El mismo commit que trasladó la clave a la bóveda también, sin hacer ruido, redujo a la mitad nuestro número de iteraciones de PBKDF2, de 100.000 a 50.000. Un solo diff, dos cambios de seguridad que apuntaban en direcciones opuestas: el titular era «firmar en el KMS», y viajando por debajo iba un factor de trabajo de hash de contraseñas reducido a la mitad, en una línea que nadie miraba porque la historia trataba de otra cosa.

Ambos son código de seguridad, así que ambos se revisaron como un único cambio «más seguro» y se aprobaron sin más gracias a la solidez de la mitad buena. La corrección fue de una línea. La lección no. Un diff etiquetado como más seguro sigue siendo un diff, y las constantes sensibles a la seguridad que contiene -número de iteraciones, tamaños de clave, tiempos de espera, nombres de algoritmos- no heredan una aureola del mensaje del commit. Ahora tratamos un número de factor de trabajo igual que tratamos la elección de un algoritmo: algo que se afirma y se verifica, no algo en lo que confías porque el cambio que lo rodeaba era una buena idea.

Vida tres: la clave que se hizo más pequeña para ser más rápida

Con la firma detrás de un KMS, cada firma es un viaje de ida y vuelta por la red, y firmar con RSA es de las operaciones caras. Así que la clave cambió de especie: RS256 sobre RSA-2048 pasó a ser ES256 sobre la curva P-256. Firmar con curva elíptica es aproximadamente un orden de magnitud más barato que con RSA, una firma P-256 ocupa 64 bytes donde la de RSA-2048 ocupa 256, y el conjunto de claves públicas que descargan los validadores encoge en consecuencia. Clave más pequeña, firma más pequeña, JWKS más pequeño, tokens más rápidos: todo por elegir una curva mejor.

La trampa es que no puedes cambiar de golpe el algoritmo de firma en un emisor en producción. Todos los tokens ya emitidos se firmaron con RS256 y tienen que seguir validándose hasta que caduquen. Cada validador tiene cacheada tu clave pública antigua. Cambia el algoritmo de un solo movimiento e invalidarás, en el mismo instante, todos los tokens en circulación y todos los conjuntos de claves cacheados: una caída autoinfligida disfrazada de mejora.

Lo que funcionó fue dejar de fingir que alguna vez hubo una sola clave. El almacén de claves se volvió agnóstico respecto al algoritmo: mantiene la clave RSA y la clave EC al mismo tiempo, y publica ambas en el JWKS, cada una bajo su propio identificador de clave y su propio algoritmo. Un selector de clave activa, independiente, decide qué clave firma los nuevos tokens, y se aparta por sí solo de la vieja clave RSA en el momento en que existe una clave EC: sin ninguna opción de configuración, sin ningún despliegue programado para coincidir con una rotación.

Durante el solapamiento, el documento de discovery anuncia ambas claves públicas, de modo que los tokens firmados con cualquiera de las dos siguen validándose. Una vez que ha caducado el último token RS256, discovery anuncia solo ES256 y la validación fija ValidAlgorithms = ES256, lo que además le cierra la puerta en las narices a los ataques de confusión de algoritmo que intentan convencer a un validador de que acepte el esquema equivocado. La migración es un fundido cruzado que el emisor realiza sobre sí mismo, no un interruptor que alguien acciona.

La lección, factorizada

Una clave de firma parece una constante: un secreto, establecido una vez, referenciado para siempre. La nuestra no lo era. A lo largo de su vida cambió de custodia, del proceso a un KMS, y cambió de especie, de RSA a curva elíptica, y ambas mudanzas hubo que hacerlas con tokens en circulación y validadores cacheando cosas que no nos pertenecen.

La propiedad que hizo sobrevivible cada mudanza fue la misma en ambas ocasiones: el sistema nunca dio por sentado que hubiera exactamente una clave, un algoritmo o un hogar. Construye el almacén de claves para que albergue varias claves y para que anuncie con honestidad cuáles alberga, y la rotación -de dónde vive la clave o de qué tipo de clave se trata- deja de ser una caída que programas y se convierte en una propiedad que el emisor gestiona por su cuenta.

Si gestionas la autenticación, aquello que firma tus tokens debería ser el objeto más aburrido, más inspeccionable y menos ingenioso que poseas. Al nuestro le hicieron falta tres vidas para llegar ahí. Valió la pena cada una de ellas.

Comments

No comments yet. Start the discussion.