Escribí una respuesta que describe en detalle cómo getrandom()
bloques esperando la entropía inicial.
Sin embargo, creo que exagera ligeramente urandom al decir que "el único instante en el que /dev/urandom podría implicar un problema de seguridad debido a la baja entropía es durante los primeros momentos de una instalación nueva y automatizada del sistema operativo".
Tus preocupaciones están bien fundadas. Tengo una pregunta abierta sobre eso mismo y sus implicaciones. El problema es que la semilla aleatoria persistente tarda bastante en pasar del grupo de entrada al grupo de salida (el grupo de bloqueo y el CRNG). Este problema significa que /dev/urandom
generará valores potencialmente predecibles durante unos minutos después del arranque. La solución es, como dices, usar el bloque /dev/random
, o para usar getrandom()
configurado para bloquear.
De hecho, no es raro ver líneas como esta en el registro del kernel al inicio temprano:
random: sn: uninitialized urandom read (4 bytes read, 7 bits of entropy available)
random: sn: uninitialized urandom read (4 bytes read, 15 bits of entropy available)
random: sn: uninitialized urandom read (4 bytes read, 16 bits of entropy available)
random: sn: uninitialized urandom read (4 bytes read, 16 bits of entropy available)
random: sn: uninitialized urandom read (4 bytes read, 20 bits of entropy available)
Todos estos son casos en los que se accedió al grupo sin bloqueo incluso antes de que se haya recopilado suficiente entropía. El problema es que la cantidad de entropía es demasiado baja para ser criptográficamente segura en este punto. Debería haber 2 valores posibles de 4 bytes, sin embargo, con solo 7 bits de entropía disponibles, eso significa que solo hay 2, o 128, posibilidades diferentes.
Halderman también parece decir que el grupo de entropía se llena en cada arranque y no, como dice Pornin en su respuesta, en la primera instalación del sistema operativo. Aunque no es muy importante para mi aplicación, me pregunto:¿cuál es?
En realidad es una cuestión de semántica. El grupo de entropía real (la página de memoria guardada en el kernel que contiene valores aleatorios) se llena en cada arranque con la semilla de entropía persistente y con el ruido ambiental. Sin embargo, la semilla de entropía en sí mismo es un archivo que se crea en el momento de la instalación y se actualiza con nuevos valores aleatorios cada vez que se apaga el sistema. Me imagino que Pornin está considerando que la semilla aleatoria es parte del grupo de entropía (como una parte del sistema general de distribución y recolección de entropía), mientras que Halderman lo considera separado (porque el grupo de entropía es técnicamente una página de memoria, nada más). La verdad es que la semilla de entropía se introduce en el grupo de entropía en cada arranque, pero puede tardar unos minutos en afectar realmente al grupo.
Un resumen de las tres fuentes de aleatoriedad:
-
/dev/random
- El dispositivo de caracteres de bloqueo disminuye un "recuento de entropía" cada vez que se lee (a pesar de que la entropía no se agota). Sin embargo, también bloquea hasta que se haya acumulado suficiente entropía en el arranque, lo que hace que sea seguro usarlo desde el principio. -
/dev/urandom
- El dispositivo de caracteres sin bloqueo generará datos aleatorios cada vez que alguien los lea. Una vez que se haya recopilado suficiente entropía, generará un flujo virtualmente ilimitado indistinguible de los datos aleatorios. Desafortunadamente, por razones de compatibilidad, es legible incluso al principio del arranque antes de que se haya recopilado suficiente entropía única. -
getrandom()
- Una llamada al sistema que generará datos aleatorios siempre que el grupo de entropía se haya inicializado correctamente con la cantidad mínima de entropía requerida. De manera predeterminada, se lee desde el grupo sin bloqueo. Si se le da elGRND_NONBLOCK
flag, devolverá un error si no hay suficiente entropía. Si se le da elGRND_RANDOM
bandera, se comportará de forma idéntica a/dev/random
, simplemente bloqueando hasta que haya entropía disponible.
Te sugiero que uses la tercera opción, la getrandom()
llamada al sistema. Esto permitirá que un proceso lea datos aleatorios criptográficamente seguros a altas velocidades, y solo se bloqueará al principio del arranque cuando no se haya recopilado suficiente entropía. Si Python os.urandom()
la función actúa como un envoltorio para esta llamada al sistema como usted dice, entonces debería estar bien para usar. Parece que en realidad hubo mucha discusión sobre si ese debería ser el caso o no, y terminó bloqueándose hasta que haya suficiente entropía disponible.
Pensando un poco más adelante:¿cuáles son las mejores prácticas para entornos que son tan frescos e ingenuos como los que describí anteriormente, pero que se ejecutan en dispositivos con perspectivas bastante abismales para la generación de entropía inicial?
Esta es una situación común y hay algunas maneras de lidiar con ella:
-
Asegúrese de bloquear en el inicio temprano, por ejemplo, usando
/dev/random
ogetrandom()
. -
Mantenga una semilla aleatoria persistente, si es posible (es decir, si puede escribir en el almacenamiento en cada arranque).
-
Lo más importante, use un RNG de hardware . Esta es la medida más efectiva #1.
El uso de un generador de números aleatorios de hardware es muy importante. El kernel de Linux inicializará su grupo de entropía con cualquier interfaz HWRNG compatible, si existe, eliminando por completo el agujero de entropía de arranque. Muchos dispositivos integrados tienen sus propios generadores de aleatoriedad.
Esto es especialmente importante para muchos dispositivos integrados, ya que es posible que no tengan un temporizador de alta resolución que se requiere para que el núcleo genere entropía de manera segura a partir del ruido ambiental. Algunas versiones de procesadores MIPS, por ejemplo, no tienen contador de ciclos.
¿Cómo y por qué sugiere usar urandom para sembrar un CSPRNG (supongo que la tierra del usuario)? ¿Cómo se hace esto al azar?
El dispositivo de aleatoriedad sin bloqueo no está diseñado para un alto rendimiento. Hasta hace poco, el dispositivo era obscenamente lento debido al uso de SHA-1 para la aleatoriedad en lugar de un cifrado de flujo como lo hace ahora. El uso de una interfaz de kernel para la aleatoriedad puede ser menos eficiente que un CSPRNG de espacio de usuario local porque cada llamada al kernel requiere un costoso cambio de contexto. El núcleo ha sido diseñado para dar cuenta de las aplicaciones que quieren sacar mucho provecho de él, pero los comentarios en el código fuente dejan claro que no ven esto como lo correcto:
/*
* Hack to deal with crazy userspace progams when they are all trying
* to access /dev/urandom in parallel. The programs are almost
* certainly doing something terribly wrong, but we'll work around
* their brain damage.
*/
Las bibliotecas criptográficas populares, como OpenSSL, admiten la generación de datos aleatorios. Se pueden sembrar una vez o volver a sembrar ocasionalmente y pueden beneficiarse más de la paralelización. Además, permite escribir código portátil que no se basa en el comportamiento de ningún sistema operativo o versión de sistema operativo en particular.
Si no necesita grandes cantidades de aleatoriedad, está completamente bien usar la interfaz del kernel. Si está desarrollando una aplicación criptográfica que necesitará mucha aleatoriedad a lo largo de su vida útil, es posible que desee utilizar una biblioteca como OpenSSL para que se encargue de eso.
Hay tres estados en los que puede estar el sistema:
- No ha recopilado suficiente entropía para inicializar de forma segura un CPRNG.
-
Ha acumulado suficiente entropía para inicializar de forma segura un CPRNG y:
2a. Ha dado más entropía de la que ha recogido.
2b. Ha dado menos entropía de la que recolecta.
Históricamente, la gente pensaba que la distinción entre (2a) y (2b) era importante. Esto causó dos problemas. Primero, está mal:la distinción no tiene sentido para un CPRNG diseñado correctamente. Y segundo, el énfasis en la distinción (2a)-vs-(2b) hizo que la gente no entendiera la distinción entre (1) y (2), que en realidad es muy importante. Las personas simplemente colapsaron (1) para convertirse en un caso especial de (2a).
Lo que realmente quieres es algo que bloquee en el estado (1) y no bloquee en los estados (2a) o (2b).
Desafortunadamente, en los viejos tiempos, la confusión entre (1) y (2a) significaba que esta no era una opción. Tus únicas dos opciones eran /dev/random
, que bloqueó en los casos (1) y (2a), y /dev/urandom
, que nunca bloqueó. Pero el estado (1) casi nunca sucede, y no sucede en absoluto en sistemas bien configurados, consulte a continuación, entonces /dev/urandom
es mejor para casi todos los sistemas, casi todo el tiempo. De ahí es de donde provienen todas esas publicaciones de blog sobre "siempre use urandom":intentaban convencer a las personas de que dejaran de hacer una distinción sin sentido y dañina entre los estados (2a) y (2b).
Pero, sí, ninguno de estos es lo que realmente quieres. Por lo tanto, el nuevo getrandom
syscall, que por defecto bloquea en el estado (1) y no bloquea en los estados (2a) o (2b). Entonces, en Linux moderno, la ortodoxia debe actualizarse a:siempre use getrandom
con la configuración predeterminada .
Arrugas adicionales:
-
getrandom
también admite un modo no predeterminado en el que actúa como/dev/random
, que se puede solicitar a través delGRND_RANDOM
bandera. AFAIK, esta bandera nunca es realmente útil, por las mismas razones descritas en las antiguas publicaciones de blog. No lo uses. -
getrandom
también tiene algunos beneficios de bonificación adicionales sobre/dev/urandom
:funciona independientemente del diseño de su sistema de archivos y no requiere abrir un descriptor de archivo, los cuales son problemáticos para las bibliotecas genéricas que quieren hacer suposiciones mínimas sobre el entorno en el que se usarán. Esto no afecta la seguridad criptográfica , pero es agradable desde el punto de vista operativo. -
Un sistema bien configurado siempre tendrá entropía disponible, incluso en el arranque temprano (es decir, nunca debería entrar en el estado (1), nunca). Hay muchas formas de gestionar esto:guardar algo de entropía del arranque anterior para usarla en el siguiente. Instale un RNG de hardware. Los contenedores Docker usan el kernel del host y, por lo tanto, obtienen acceso a su grupo de entropía. Las configuraciones de virtualización de alta calidad tienen formas de permitir que el sistema invitado obtenga entropía del sistema host a través de interfaces de hipervisor (por ejemplo, busque "virtio rng"). Pero, por supuesto, no todos los sistemas están bien configurados. Si tiene un sistema mal configurado, debería ver si puede configurarlo bien. En principio, esto debería ser fácil y económico, pero en realidad las personas no priorizan la seguridad, por lo que podría requerir hacer cosas como cambiar de proveedor de nube o cambiar a una plataforma integrada diferente. Y, lamentablemente, es posible que descubra que esto es más caro de lo que usted (o su jefe) está dispuesto a pagar, por lo que debe lidiar con un sistema mal configurado. Mis condolencias si es así.
-
Como señala @forest, si necesita muchos valores de CPRNG, entonces, si tiene mucho cuidado, puede acelerar esto ejecutando su propio CPRNG en el espacio de usuario, mientras usa
getrandom
para (re)sembrar. Sin embargo, esto es en gran medida una cosa "solo para expertos", al igual que cualquier situación en la que se encuentre implementando sus propias primitivas criptográficas. Solo debe hacerlo si ha medido y encontrado que usandogetrandom
directamente es demasiado lento para sus necesidades y usted tiene una gran experiencia criptográfica. Es muy fácil arruinar una implementación de CPRNG de tal manera que su seguridad esté totalmente rota, pero la salida aún "parece" aleatoria para que no se dé cuenta.