Ho scritto una risposta che descrive in dettaglio come getrandom()
blocchi in attesa dell'entropia iniziale.
Tuttavia, penso che sopravvaluti leggermente urandom dicendo che "l'unico istante in cui /dev/urandom potrebbe implicare un problema di sicurezza a causa della bassa entropia è durante i primi momenti di una nuova installazione automatica del sistema operativo".
Le tue preoccupazioni sono fondate. Ho una domanda aperta proprio su questa cosa e sulle sue implicazioni. Il problema è che il seme casuale persistente impiega un po' di tempo per passare dal pool di input al pool di output (il pool di blocco e il CRNG). Questo problema significa che /dev/urandom
produrrà valori potenzialmente prevedibili per alcuni minuti dopo l'avvio. La soluzione è, come dici tu, usare il blocco /dev/random
o per utilizzare getrandom()
impostato su blocco.
Infatti, non è raro vedere righe come questa nel registro del kernel all'avvio iniziale:
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)
Tutti questi sono casi in cui è stato effettuato l'accesso al pool non bloccante anche prima che fosse stata raccolta abbastanza entropia. Il problema è che la quantità di entropia è troppo bassa per essere sufficientemente crittograficamente sicura a questo punto. Dovrebbero esserci 2 possibili valori di 4 byte, tuttavia con solo 7 bit di entropia disponibili, ciò significa che ci sono solo 2, o 128, diverse possibilità.
Halderman sembra anche dire che il pool di entropia si riempie ad ogni avvio e non, come dice Pornin nella sua risposta, alla primissima installazione del sistema operativo. Anche se non è molto importante per la mia domanda, mi chiedo:qual è?
In realtà è una questione di semantica. L'attuale piscina di entropia (la pagina di memoria conservata nel kernel che contiene valori casuali) viene riempita ad ogni avvio dal seme di entropia persistente e dal rumore ambientale. Tuttavia, l'entropia seme stesso è un file che viene creato al momento dell'installazione e viene aggiornato con nuovi valori casuali ogni volta che il sistema si spegne. Immagino che Pornin stia considerando il seme casuale come parte del pool di entropia (come in, una parte del sistema generale di distribuzione e raccolta dell'entropia), mentre Halderman lo considera separato (perché il pool di entropia è tecnicamente una pagina di memoria, niente di più). La verità è che il seme di entropia viene immesso nel pool di entropia a ogni avvio, ma possono essere necessari alcuni minuti per influenzare effettivamente il pool.
Un riepilogo delle tre fonti di casualità:
-
/dev/random
- Il dispositivo di blocco dei caratteri decrementa un "conteggio di entropia" ogni volta che viene letto (nonostante l'entropia non sia effettivamente esaurita). Tuttavia, si blocca anche fino a quando non viene raccolta entropia sufficiente all'avvio, rendendone sicuro l'utilizzo nelle prime fasi. -
/dev/urandom
- Il dispositivo di carattere non bloccante emetterà dati casuali ogni volta che qualcuno legge da esso. Una volta raccolta sufficiente entropia, produrrà un flusso virtualmente illimitato indistinguibile dai dati casuali. Sfortunatamente, per motivi di compatibilità, è leggibile anche all'inizio dell'avvio prima che sia stata raccolta una quantità sufficiente di entropia una tantum. -
getrandom()
- Una chiamata di sistema che produrrà dati casuali fintanto che il pool di entropia è stato correttamente inizializzato con la quantità minima di entropia richiesta. L'impostazione predefinita è la lettura dal pool non bloccante. Se viene fornito ilGRND_NONBLOCK
flag, restituirà un errore se non c'è abbastanza entropia. Se viene fornitoGRND_RANDOM
flag, si comporterà in modo identico a/dev/random
, semplicemente bloccando finché non è disponibile entropia.
Ti suggerisco di utilizzare la terza opzione, il getrandom()
syscall. Ciò consentirà a un processo di leggere dati casuali crittograficamente sicuri ad alta velocità e si bloccherà solo all'inizio dell'avvio quando non è stata raccolta abbastanza entropia. Se os.urandom()
di Python function funge da wrapper per questa chiamata di sistema come dici tu, quindi dovrebbe andare bene da usare. Sembra che in realtà ci siano state molte discussioni sull'opportunità o meno di farlo, finendo per bloccarlo fino a quando non sarà disponibile abbastanza entropia.
Pensando un po' più in là:quali sono le migliori pratiche per ambienti freschi e ingenui come ho descritto sopra, ma che girano su dispositivi con prospettive piuttosto spaventose per la generazione iniziale di entropia?
Questa è una situazione comune e ci sono alcuni modi per affrontarla:
-
Assicurati di bloccare all'avvio anticipato, ad esempio utilizzando
/dev/random
ogetrandom()
. -
Mantieni un seme casuale persistente, se possibile (ovvero se puoi scrivere nella memoria ad ogni avvio).
-
Soprattutto, usa un RNG hardware . Questa è la prima misura più efficace.
L'utilizzo di un generatore di numeri casuali hardware è molto importante. Il kernel Linux inizializzerà il suo pool di entropia con qualsiasi interfaccia HWRNG supportata se ne esiste una, eliminando completamente il buco di entropia di avvio. Molti dispositivi integrati hanno i propri generatori di casualità.
Ciò è particolarmente importante per molti dispositivi embedded, poiché potrebbero non disporre di un timer ad alta risoluzione necessario affinché il kernel generi in modo sicuro entropia dal rumore ambientale. Alcune versioni dei processori MIPS, ad esempio, non hanno un contatore di cicli.
Come e perché suggerisci di usare urandom per seminare un (immagino userland?) CSPRNG? In che modo questo battito diventa casuale?
Il dispositivo di casualità non bloccante non è progettato per prestazioni elevate. Fino a poco tempo fa, il dispositivo era oscenamente lento a causa dell'utilizzo di SHA-1 per la casualità piuttosto che di un codice a flusso come avviene ora. L'utilizzo di un'interfaccia del kernel per la casualità può essere meno efficiente di un CSPRNG locale in spazio utente perché ogni chiamata al kernel richiede un costoso cambio di contesto. Il kernel è stato progettato per tenere conto delle applicazioni che vogliono attingere pesantemente da esso, ma i commenti nel codice sorgente chiariscono che non la vedono come la cosa giusta da fare:
/*
* 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.
*/
Librerie crittografiche popolari come OpenSSL supportano la generazione di dati casuali. Possono essere seminati una volta o riseminati occasionalmente e sono in grado di trarre maggiori vantaggi dalla parallelizzazione. Consente inoltre di scrivere codice portabile che non si basa sul comportamento di un particolare sistema operativo o versione del sistema operativo.
Se non hai bisogno di enormi quantità di casualità, va benissimo usare l'interfaccia del kernel. Se stai sviluppando un'applicazione crittografica che richiederà molta casualità per tutta la sua durata, potresti voler utilizzare una libreria come OpenSSL per gestirla per te.
Ci sono tre stati in cui il sistema può trovarsi:
- Non ha raccolto abbastanza entropia per inizializzare in sicurezza un CPRNG.
-
Ha raccolto abbastanza entropia per inizializzare in sicurezza un CPRNG, e:
2a. Ha emesso più entropia di quanta ne abbia raccolta.
2b. Ha emesso meno entropia di quanta ne ha raccolta.
Storicamente, la gente pensava che la distinzione tra (2a) e (2b) fosse importante. Ciò ha causato due problemi. Primo, è sbagliato:la distinzione non ha senso per un CPRNG progettato correttamente. E in secondo luogo, l'enfasi sulla distinzione (2a)-vs-(2b) ha fatto perdere alle persone la distinzione tra (1) e (2), che in realtà è davvero importante. Le persone sono semplicemente crollate (1) in un caso speciale di (2a).
Quello che vuoi veramente è qualcosa che blocchi nello stato (1) e non blocchi negli stati (2a) o (2b).
Sfortunatamente, ai vecchi tempi, la confusione tra (1) e (2a) significava che questa non era un'opzione. Le tue uniche due opzioni erano /dev/random
, che bloccava nei casi (1) e (2a), e /dev/urandom
, che non si è mai bloccato. Ma lo stato (1) non si verifica quasi mai - e non si verifica affatto in sistemi ben configurati, vedi sotto - quindi /dev/urandom
è migliore per quasi tutti i sistemi, quasi sempre. Ecco da dove provengono tutti quei post sul blog su "usa sempre urandom":stavano cercando di convincere le persone a smettere di fare una distinzione priva di significato e dannosa tra gli stati (2a) e (2b).
Ma, sì, nessuno di questi è ciò che vuoi veramente. Pertanto, il più recente getrandom
syscall, che per impostazione predefinita si blocca nello stato (1) e non si blocca negli stati (2a) o (2b). Quindi su Linux moderno, l'ortodossia dovrebbe essere aggiornata a:usa sempre getrandom
con le impostazioni predefinite .
Rughe extra:
-
getrandom
supporta anche una modalità non predefinita in cui si comporta come/dev/random
, che può essere richiesto tramite ilGRND_RANDOM
bandiera. AFAIK questo flag non è mai effettivamente utile, per tutti gli stessi motivi descritti da quei vecchi post sul blog. Non usarlo. -
getrandom
ha anche alcuni bonus extra rispetto a/dev/urandom
:funziona indipendentemente dal layout del file system e non richiede l'apertura di un descrittore di file, entrambi problematici per le librerie generiche che desiderano fare ipotesi minime sull'ambiente in cui verranno utilizzate. Ciò non influisce sulla sicurezza crittografica , ma funziona bene. -
Un sistema ben configurato avrà sempre entropia disponibile, anche all'avvio anticipato (ovvero, non dovresti mai entrare nello stato (1), mai). Ci sono molti modi per gestirlo:salva un po' di entropia dall'avvio precedente per usarla in quello successivo. Installa un RNG hardware. I contenitori Docker utilizzano il kernel dell'host e quindi ottengono l'accesso al suo pool di entropia. Le configurazioni di virtualizzazione di alta qualità hanno modi per consentire al sistema guest di recuperare l'entropia dal sistema host tramite interfacce hypervisor (ad esempio, cercare "virtio rng"). Ma ovviamente non tutti i sistemi sono ben configurati. Se hai un sistema mal configurato, dovresti vedere se riesci invece a farlo ben configurato. In linea di principio questo dovrebbe essere economico, ma in realtà le persone non danno la priorità alla sicurezza, quindi ... potrebbe richiedere di fare cose come cambiare fornitore di servizi cloud o passare a una piattaforma integrata diversa. E sfortunatamente, potresti scoprire che questo è più costoso di quanto tu (o il tuo capo) sia disposto a pagare, quindi sei bloccato a gestire un sistema mal configurato. Le mie condoglianze se è così.
-
Come osserva @forest, se hai bisogno di molti valori CPRNG, se stai molto attento puoi velocizzarlo eseguendo il tuo CPRNG nello spazio utente, mentre usi
getrandom
per (ri)semina. Tuttavia, questa è una cosa "solo per esperti", proprio come qualsiasi situazione in cui ti ritrovi a implementare le tue primitive crittografiche. Dovresti farlo solo se l'hai misurato e trovato usandogetrandom
direct è troppo lento per le tue esigenze e hai una significativa esperienza crittografica. È molto facile rovinare un'implementazione CPRNG in modo tale da violare totalmente la sicurezza, ma l'output "sembra" ancora casuale, quindi non te ne accorgi.