/dev/random su Linux: kernel e casualità

TLDR;

/dev/random è il punto di accesso al sottosistema di generazione di entropia del kernel Linux. Raccoglie rumore fisico dall’hardware, lo accumula in un entropy pool, e lo usa come seme per un CSPRNG basato su ChaCha20 che produce uno stream praticamente infinito di byte imprevedibili. Dalla versione 5.6 del kernel, la distinzione operativa tra /dev/random e /dev/urandom è sostanzialmente irrilevante su hardware moderno. Per il codice applicativo, usare sempre le API ad alto livello del proprio linguaggio (secrets in Python, crypto in Node.js) che internamente si appoggiano a questo sottosistema.

/dev/random – cos’è e perché esiste

I computer sono macchine deterministiche: dato lo stesso input, producono sempre lo stesso output. Questo li rende fondamentalmente inadatti a generare vera casualità. Eppure la casualità è critica in crittografia: salt per le password, session ID, chiavi TLS: tutti richiedono numeri imprevedibili. Linux risolve questo problema con due pseudo-device speciali: /dev/random e /dev/urandom, presenti dal kernel 1.3.30. Leggerli restituisce byte casuali crittograficamente sicuri.
Manpage Debian sull’argomento.

L’Entropy Pool

Il cuore del sistema è l’entropy pool: un serbatoio di bit di rumore che il kernel raccoglie continuamente da sorgenti fisiche imprevedibili:

  • Interrupt hardware: timing dei pacchetti di rete, interrupt del disco
  • Input utente: jitter nei tempi di pressione dei tasti e movimenti del mouse
  • Jitter del clock CPU (algoritmo jitterentropy): variazioni microscopiche nei cicli di clock dovute a effetti termici e fisici
  • Istruzione RDRAND: sorgente hardware di entropia presente in quasi tutti i processori x86-64 moderni

Il kernel tiene traccia di quanti bit di entropia stima di aver accumulato nel pool. Questo valore è visibile in qualsiasi momento:

cat /proc/sys/kernel/random/entropy_avail
# es: 512

cat /proc/sys/kernel/random/poolsize
# es: 4096

/dev/random vs /dev/urandom: la distinzione storica

Storicamente i due device avevano comportamenti diversi:

  • /dev/random: bloccante –> se l’entropy pool si svuotava, si fermava ad aspettare nuova entropia prima di restituire altri byte
  • /dev/urandom: non bloccante –> continuava a generare byte anche con poca entropia, usando il CSPRNG interno in modo autonomo

Questa distinzione aveva senso nelle architetture precedenti. Da Linux kernel 5.6 (2020) i due device si comportano in modo praticamente identico: /dev/random blocca solo durante l’inizializzazione del CSPRNG al boot, non dopo. Una volta inizializzato, entrambi usano lo stesso generatore sottostante.

Pagina Wikipedia: [en.wikipedia.org/wiki/dev/random]

Su sistemi x86-64 moderni (praticamente tutto l’hardware attuale), grazie alla presenza garantita di RDTSC e RDRAND, i due sono equivalenti.

Il CSPRNG interno: ChaCha20

Dal kernel 4.8 (ottobre 2016), il generatore sottostante è stato riscritto da Theodore Ts’o usando ChaCha20, un cifrario a stream moderno progettato da Daniel Bernstein. Il funzionamento in sintesi è:

  1. L’entropy pool viene usato come seme (seed) per inizializzare lo stato interno del CSPRNG
  2. Il CSPRNG genera uno stream di byte pseudocasuali a partire da quel seme
  3. Lo stato interno viene periodicamente rimescolato con nuova entropia dall’hardware
  4. Senza conoscere lo stato interno, è computazionalmente impossibile predire o invertire l’output

Questa architettura garantisce che anche se un attaccante osserva l’output del generatore, non può risalire allo stato interno né prevedere i byte futuri.

Come usarlo in pratica

Da shell

# Genera 32 byte casuali in hex (utile per chiavi, token)
head -c 32 /dev/urandom | xxd -p

# Oppure con od
head -c 16 /dev/urandom | od -A n -t x1 | tr -d ' \n'

Da Python

import os
import secrets  # interfaccia ad alto livello raccomandata

# Basso livello: legge direttamente dal CSPRNG del kernel
token = os.urandom(32)

# Alto livello: modulo secrets (Python 3.6+), raccomandato per uso crittografico
session_id = secrets.token_hex(32)   # 64 caratteri hex
token_url  = secrets.token_urlsafe(32)  # stringa URL-safe base64

Da Node.js

const crypto = require('crypto');

// Genera session ID di 128 bit (16 byte = 32 hex chars)
const sessionId = crypto.randomBytes(16).toString('hex');

Interfaccia syscall raccomandata

Per applicazioni che richiedono massima portabilità e correttezza, la via raccomandata è la syscall getrandom(2) invece di leggere i file direttamente. Tutti i linguaggi moderni la usano internamente nei loro moduli crittografici.


Il problema all’avvio (boot entropy)

Il momento più critico è il boot di un sistema senza interazione utente (tipico dei server): l’entropy pool può trovarsi in uno stato parzialmente prevedibile perché non ci sono ancora movimenti del mouse, pressioni di tasti o attività disco (dischi rotazionali; gli SSD hanno annullato questo fattore).

Soluzioni:

  • RDRAND/RDSEED: istruzioni hardware che forniscono entropia immediata al boot su CPU Intel/AMD moderne
  • jitterentropy: sfrutta il jitter del clock CPU, disponibile anche su hardware privo di RDRAND
  • Seed file: alcune distribuzioni salvano lo stato del CSPRNG a ogni shutdown e lo ricaricano al boot successivo (/var/lib/systemd/random-seed)
  • virtio-rng: su macchine virtuali, il kernel guest può attingere all’entropy pool dell’host hypervisor

Perché è rilevante per la sicurezza applicativa

Collegandosi all’articolo precedente sui principi fondamentali dell’autenticazione e sul caso DJI: session ID, salt per bcrypt/Argon2, token di reset password e chiavi di sessione devono provenire da un CSPRNG; ovvero, alla fine della catena, da /dev/random o equivalente.
Usare rand() del C, Math.random() di JavaScript o qualsiasi PRNG non crittografico per questi scopi è una vulnerabilità grave: un attaccante che osserva abbastanza output può ricostruire lo stato interno del generatore e predire i token futuri.


Pubblicato in Linux.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *