有很多關於 /dev/urandom
和 /dev/random
的流言在坊間不斷流傳。然而流言終究是流言。
本篇文章裡針對的都是近來的 Linux 作業系統,其它類 Unix 作業系統不在討論範圍內。
/dev/urandom
不安全。加密用途必須使用 /dev/random
事實:/dev/urandom
才是類 Unix 作業系統下推薦的加密種子。
/dev/urandom
是偽亂數生成器(PRND),而 /dev/random
是“真”亂數生成器。
事實:它們兩者本質上用的是同一種 CSPRNG (一種密碼學偽亂數生成器)。它們之間細微的差別和“真”“不真”隨機完全無關。(參見:“Linux 亂數生成器的構架”一節)
/dev/random
在任何情況下都是密碼學應用更好地選擇。即便 /dev/urandom
也同樣安全,我們還是不應該用它。
事實:/dev/random
有個很噁心人的問題:它是阻塞的。(參見:“阻塞有什麼問題?”一節)(LCTT 譯註:意味著請求都得逐個執行,等待前一個請求完成)
但阻塞不是好事嗎!/dev/random
只會給出電腦收集的資訊熵足以支援的隨機量。/dev/urandom
在用完了所有熵的情況下還會不斷吐出不安全的亂數給你。
事實:這是誤解。就算我們不去考慮應用層面後續對隨機種子的用法,“用完資訊熵池”這個概念本身就不存在。僅僅 256 位的熵就足以生成計算上安全的亂數很長、很長的一段時間了。(參見:“那熵池快空了的情況呢?”一節)
問題的關鍵還在後頭:/dev/random
怎麼知道有系統會多少可用的資訊熵?接著看!
但密碼學家老是討論重新選種子(re-seeding)。這難道不和上一條衝突嗎?
事實:你說的也沒錯!某種程度上吧。確實,亂數生成器一直在使用系統資訊熵的狀態重新選種。但這麼做(一部分)是因為別的原因。(參見:“重新選種”一節)
這樣說吧,我沒有說引入新的資訊熵是壞的。更多的熵肯定更好。我只是說在熵池低的時候阻塞是沒必要的。
好,就算你說的都對,但是 /dev/(u)random
的 man 頁面和你說的也不一樣啊!到底有沒有專家同意你說的這堆啊?
事實:其實 man 頁面和我說的不衝突。它看似好像在說 /dev/urandom
對密碼學用途來說不安全,但如果你真的理解這堆密碼學術語你就知道它說的並不是這個意思。(參見:“random 和 urandom 的 man 頁面”一節)
man 頁面確實說在一些情況下推薦使用 /dev/random
(我覺得也沒問題,但絕對不是說必要的),但它也推薦在大多數“一般”的密碼學應用下使用 /dev/urandom
。
雖然訴諸權威一般來說不是好事,但在密碼學這麼嚴肅的事情上,和專家統一意見是很有必要的。
所以說呢,還確實有一些專家和我的一件事一致的:/dev/urandom
就應該是類 UNIX 作業系統下密碼學應用的首選。顯然的,是他們的觀點說服了我而不是反過來的。(參見:“正道”一節)
難以相信嗎?覺得我肯定錯了?讀下去看我能不能說服你。
我嘗試不講太高深的東西,但是有兩點內容必須先提一下才能讓我們接著論證觀點。
首當其衝的,什麼是隨機性,或者更準確地:我們在探討什麼樣的隨機性?(參見:“真隨機”一節)
另外一點很重要的是,我沒有嘗試以說教的態度對你們寫這段話。我寫這篇文章是為了日後可以在討論起的時候指給別人看。比 140 字長(LCTT 譯註:推特長度)。這樣我就不用一遍遍重複我的觀點了。能把論點磨鍊成一篇文章本身就很有助於將來的討論。(參見:“你是在說我笨?!”一節)
並且我非常樂意聽到不一樣的觀點。但我只是認為單單地說 /dev/urandom
壞是不夠的。你得能指出到底有什麼問題,並且剖析它們。
絕對沒有!
事實上我自己也相信了 “/dev/urandom
是不安全的” 好些年。這幾乎不是我們的錯,因為那麼德高望重的人在 Usenet、論壇、推特上跟我們重複這個觀點。甚至連 man 手冊都似是而非地說著。我們當年怎麼可能鄙視諸如“資訊熵太低了”這種看上去就很讓人信服的觀點呢?(參見:“random 和 urandom 的 man 頁面”一節)
整個流言之所以如此廣為流傳不是因為人們太蠢,而是因為但凡有點關於資訊熵和密碼學概念的人都會覺得這個說法很有道理。直覺似乎都在告訴我們這流言講的很有道理。很不幸直覺在密碼學裡通常不管用,這次也一樣。
亂數是“真正隨機”是什麼意思?
我不想搞的太複雜以至於變成哲學範疇的東西。這種討論很容易走偏因為對於隨機模型大家見仁見智,討論很快變得毫無意義。
在我看來“真隨機”的“試金石”是量子效應。一個光子穿過或不穿過一個半透鏡。或者觀察一個放射性粒子衰變。這類東西是現實世界最接近真隨機的東西。當然,有些人也不相信這類過程是真隨機的,或者這個世界根本不存在任何隨機性。這個就百家爭鳴了,我也不好多說什麼了。
密碼學家一般都會通過不去討論什麼是“真隨機”來避免這種哲學辯論。他們更關心的是不可預測性。只要沒有任何方法能猜出下一個亂數就可以了。所以當你以密碼學應用為前提討論一個亂數好不好的時候,在我看來這才是最重要的。
無論如何,我不怎麼關心“哲學上安全”的亂數,這也包括別人嘴裡的“真”亂數。
但就讓我們退一步說,你有了一個“真”隨機變數。你下一步做什麼呢?
你把它們列印出來然後掛在牆上來展示量子宇宙的美與和諧?牛逼!我支援你。
但是等等,你說你要用它們?做密碼學用途?額,那這就廢了,因為這事情就有點複雜了。
事情是這樣的,你的真隨機、量子力學加護的亂數即將被用進不理想的現實世界演算法裡去。
因為我們使用的幾乎所有的演算法都並不是資訊理論安全性的。它們“只能”提供計算意義上的安全。我能想到為數不多的例外就只有 Shamir 金鑰分享和一次性密碼本(OTP)演算法。並且就算前者是名副其實的(如果你實際打算用的話),後者則毫無可行性可言。
但所有那些大名鼎鼎的密碼學演算法,AES、RSA、Diffie-Hellman、橢圓曲線,還有所有那些加密軟體包,OpenSSL、GnuTLS、Keyczar、你的作業系統的加密 API,都僅僅是計算意義上安全的。
那區別是什麼呢?資訊理論安全的演算法肯定是安全的,絕對是,其它那些的演算法都可能在理論上被擁有無限計算力的窮舉破解。我們依然愉快地使用它們是因為全世界的計算機加起來都不可能在宇宙年齡的時間裡破解,至少現在是這樣。而這就是我們文章裡說的“不安全”。
除非哪個聰明的傢伙破解了演算法本身 —— 在只需要更少量計算力、在今天可實現的計算力的情況下。這也是每個密碼學家夢寐以求的聖杯:破解 AES 本身、破解 RSA 本身等等。
所以現在我們來到了更底層的東西:亂數生成器,你堅持要“真隨機”而不是“偽隨機”。但是沒過一會兒你的真亂數就被餵進了你極為鄙視的偽隨機演算法裡了!
真相是,如果我們最先進的雜湊演算法被破解了,或者最先進的分組加密演算法被破解了,你得到的這些“哲學上不安全”的亂數甚至無所謂了,因為反正你也沒有安全的應用方法了。
所以把計算性上安全的亂數餵給你的僅僅是計算性上安全的演算法就可以了,換而言之,用 /dev/urandom
。
你對核心的亂數生成器的理解很可能是像這樣的:
“真正的隨機性”,儘管可能有點瑕疵,進入作業系統然後它的熵立刻被加入內部熵計數器。然後經過“矯偏”和“漂白”之後它進入核心的熵池,然後 /dev/random
和 /dev/urandom
從裡面生成亂數。
“真”亂數生成器,/dev/random
,直接從池裡選出亂數,如果熵計數器表示能滿足需要的數位大小,那就吐出數位並且減少熵計數。如果不夠的話,它會阻塞程式直至有足夠的熵進入系統。
這裡很重要一環是 /dev/random
幾乎只是僅經過必要的“漂白”後就直接把那些進入系統的隨機性吐了出來,不經扭曲。
而對 /dev/urandom
來說,事情是一樣的。除了當沒有足夠的熵的時候,它不會阻塞,而會從一直在執行的偽亂數生成器(當然,是密碼學安全的,CSPRNG)裡吐出“低品質”的亂數。這個 CSPRNG 只會用“真亂數”生成種子一次(或者好幾次,這不重要),但你不能特別相信它。
在這種對亂數生成的理解下,很多人會覺得在 Linux 下盡量避免 /dev/urandom
看上去有那麼點道理。
因為要麼你有足夠多的熵,你會相當於用了 /dev/random
。要麼沒有,那你就會從幾乎沒有高熵輸入的 CSPRNG 那裡得到一個低品質的亂數。
看上去很邪惡是吧?很不幸的是這種看法是完全錯誤的。實際上,亂數生成器的構架更像是下面這樣的。
你看到最大的區別了嗎?CSPRNG 並不是和亂數生成器一起跑的,它在 /dev/urandom
需要輸出但熵不夠的時候進行填充。CSPRNG 是整個亂數生成過程的內部元件之一。從來就沒有什麼 /dev/random
直接從池裡輸出純純的隨機性。每個隨機源的輸入都在 CSPRNG 裡充分混合和雜湊過了,這一切都發生在實際變成一個亂數,被 /dev/urandom
或者 /dev/random
吐出去之前。
另外一個重要的區別是這裡沒有熵計數器的任何事情,只有預估。一個源給你的熵的量並不是什麼很明確能直接得到的數位。你得預估它。注意,如果你太樂觀地預估了它,那 /dev/random
最重要的特性——只給出熵允許的隨機量——就蕩然無存了。很不幸的,預估熵的量是很困難的。
這是個很粗糙的簡化。實際上不僅有一個,而是三個熵池。一個主池,另一個給
/dev/random
,還有一個給/dev/urandom
,後兩者依靠從主池裡獲取熵。這三個池都有各自的熵計數器,但二級池(後兩個)的計數器基本都在 0 附近,而“新鮮”的熵總在需要的時候從主池流過來。同時還有好多混合和回流進系統在同時進行。整個過程對於這篇文件來說都過於複雜了,我們跳過。
Linux 核心只使用事件的到達時間來預估熵的量。根據模型,它通過多項式插值來預估實際的到達時間有多“出乎意料”。這種多項式插值的方法到底是不是好的預估熵量的方法本身就是個問題。同時硬體情況會不會以某種特定的方式影響到達時間也是個問題。而所有硬體的取樣率也是個問題,因為這基本上就直接決定了亂數到達時間的顆粒度。
說到最後,至少現在看來,核心的熵預估還是不錯的。這也意味著它比較保守。有些人會具體地討論它有多好,這都超出我的腦容量了。就算這樣,如果你堅持不想在沒有足夠多的熵的情況下吐出亂數,那你看到這裡可能還會有一絲緊張。我睡的就很香了,因為我不關心熵預估什麼的。
最後要明確一下:/dev/random
和 /dev/urandom
都是被同一個 CSPRNG 飼餵的。只有它們在用完各自熵池(根據某種預估標準)的時候,它們的行為會不同:/dev/random
阻塞,/dev/urandom
不阻塞。
在 Linux 4.8 裡,/dev/random
和 /dev/urandom
的等價性被放棄了。現在 /dev/urandom
的輸出不來自於熵池,而是直接從 CSPRNG 來。
我們很快會理解為什麼這不是一個安全問題。(參見:“CSPRNG 沒問題”一節)
你有沒有需要等著 /dev/random
來吐亂數?比如在虛擬機器裡生成一個 PGP 金鑰?或者存取一個在生成對談金鑰的網站?
這些都是問題。阻塞本質上會降低可用性。換而言之你的系統不干你讓它乾的事情。不用我說,這是不好的。要是它不幹活你幹嘛搭建它呢?
我在工廠自動化裡做過和安全相關的系統。猜猜看安全系統失效的主要原因是什麼?操作問題。就這麼簡單。很多安全措施的流程讓工人惱火了。比如時間太長,或者太不方便。你要知道人很會找捷徑來“解決”問題。
但其實有個更深刻的問題:人們不喜歡被打斷。它們會找一些繞過的方法,把一些詭異的東西接在一起僅僅因為這樣能用。一般人根本不知道什麼密碼學什麼亂七八糟的,至少正常的人是這樣吧。
為什麼不禁止呼叫 random()
?為什麼不隨便在論壇上找個人告訴你用寫奇異的 ioctl 來增加熵計數器呢?為什麼不乾脆就把 SSL 加密給關了算了呢?
到頭來如果東西太難用的話,你的使用者就會被迫開始做一些降低系統安全性的事情——你甚至不知道它們會做些什麼。
我們很容易會忽視可用性之類的重要性。畢竟安全第一對吧?所以比起犧牲安全,不可用、難用、不方便都是次要的?
這種二元對立的想法是錯的。阻塞不一定就安全了。正如我們看到的,/dev/urandom
直接從 CSPRNG 裡給你一樣好的亂數。用它不好嗎!
現在情況聽上去很慘淡。如果連高品質的 /dev/random
都是從一個 CSPRNG 裡來的,我們怎麼敢在高安全性的需求上使用它呢?
實際上,“看上去隨機”是現存大多數密碼學基礎元件的基本要求。如果你觀察一個密碼學雜湊的輸出,它一定得和隨機的字串不可區分,密碼學家才會認可這個演算法。如果你生成一個分組加密,它的輸出(在你不知道金鑰的情況下)也必須和亂數據不可區分才行。
如果任何人能比暴力窮舉要更有效地破解一個加密,比如它利用了某些 CSPRNG 偽隨機的弱點,那這就又是老一套了:一切都廢了,也別談後面的了。分組加密、雜湊,一切都是基於某個數學演算法,比如 CSPRNG。所以別害怕,到頭來都一樣。
毫無影響。
加密演算法的根基建立在攻擊者不能預測輸出上,只要最一開始有足夠的隨機性(熵)就行了。“足夠”的下限可以是 256 位,不需要更多了。
介於我們一直在很隨意的使用“熵”這個概念,我用“位”來量化隨機性希望讀者不要太在意細節。像我們之前討論的那樣,核心的亂數生成器甚至沒法精確地知道進入系統的熵的量。只有一個預估。而且這個預估的準確性到底怎麼樣也沒人知道。
但如果熵這麼不重要,為什麼還要有新的熵一直被收進亂數生成器裡呢?
djb 提到 太多的熵甚至可能會起到反效果。
首先,一般不會這樣。如果你有很多隨機性可以拿來用,用就對了!
但亂數生成器時不時要重新選種還有別的原因:
想象一下如果有個攻擊者獲取了你亂數生成器的所有內部狀態。這是最壞的情況了,本質上你的一切都暴露給攻擊者了。
你已經涼了,因為攻擊者可以計算出所有未來會被輸出的亂數了。
但是,如果不斷有新的熵被混進系統,那內部狀態會再一次變得隨機起來。所以亂數生成器被設計成這樣有些“自癒”能力。
但這是在給內部狀態引入新的熵,這和阻塞輸出沒有任何關係。
這兩個 man 頁面在嚇唬程式設計師方面很有建樹:
從
/dev/urandom
讀取資料不會因為需要更多熵而阻塞。這樣的結果是,如果熵池裡沒有足夠多的熵,取決於驅動使用的演算法,返回的數值在理論上有被密碼學攻擊的可能性。發動這樣攻擊的步驟並沒有出現在任何公開文獻當中,但這樣的攻擊從理論上講是可能存在的。如果你的應用擔心這類情況,你應該使用/dev/random
。實際上已經有
/dev/random
和/dev/urandom
的 Linux 核心 man 頁面的更新版本。不幸的是,隨便一個網路搜尋出現我在結果頂部的仍然是舊的、有缺陷的版本。此外,許多 Linux 發行版仍在發布舊的 man 頁面。所以不幸的是,這一節需要在這篇文章中保留更長的時間。我很期待刪除這一節!
沒有“公開的文獻”描述,但是 NSA 的小賣部裡肯定賣這種攻擊手段是吧?如果你真的真的很擔心(你應該很擔心),那就用 /dev/random
然後所有問題都沒了?
然而事實是,可能某個什麼情報局有這種攻擊,或者某個什麼邪惡駭客組織找到了方法。但如果我們就直接假設這種攻擊一定存在也是不合理的。
而且就算你想給自己一個安心,我要給你潑個冷水:AES、SHA-3 或者其它什麼常見的加密演算法也沒有“公開文獻記述”的攻擊手段。難道你也不用這幾個加密演算法了?這顯然是可笑的。
我們在回到 man 頁面說:“使用 /dev/random
”。我們已經知道了,雖然 /dev/urandom
不阻塞,但是它的亂數和 /dev/random
都是從同一個 CSPRNG 裡來的。
如果你真的需要資訊理論安全性的亂數(你不需要的,相信我),那才有可能成為唯一一個你需要等足夠熵進入 CSPRNG 的理由。而且你也不能用 /dev/random
。
man 頁面有毒,就這樣。但至少它還稍稍挽回了一下自己:
如果你不確定該用
/dev/random
還是/dev/urandom
,那你可能應該用後者。通常來說,除了需要長期使用的 GPG/SSL/SSH 金鑰以外,你總該使用/dev/urandom
。該手冊頁的當前更新版本毫不含糊地說:
/dev/random
介面被認為是遺留介面,並且/dev/urandom
在所有用例中都是首選和足夠的,除了在啟動早期需要隨機性的應用程式;對於這些應用程式,必須替代使用getrandom(2)
,因為它將阻塞,直到熵池初始化完成。
行。我覺得沒必要,但如果你真的要用 /dev/random
來生成 “長期使用的金鑰”,用就是了也沒人攔著!你可能需要等幾秒鐘或者敲幾下鍵盤來增加熵,但這沒什麼問題。
但求求你們,不要就因為“你想更安全點”就讓連個郵件伺服器要掛起半天。
本篇文章裡的觀點顯然在網際網路上是“小眾”的。但如果問一個真正的密碼學家,你很難找到一個認同阻塞 /dev/random
的人。
比如我們看看 Daniel Bernstein(即著名的 djb)的看法:
我們密碼學家對這種胡亂迷信行為表示不負責。你想想,寫
/dev/random
man 頁面的人好像同時相信:
- (1) 我們不知道如何用一個 256 位長的
/dev/random
的輸出來生成一個無限長的隨機金鑰串流(這是我們需要/dev/urandom
吐出來的),但與此同時- (2) 我們卻知道怎麼用單個金鑰來加密一條訊息(這是 SSL,PGP 之類幹的事情)
對密碼學家來說這甚至都不好笑了
或者 Thomas Pornin 的看法,他也是我在 stackexchange 上見過最樂於助人的一位:
簡單來說,是的。展開說,答案還是一樣。
/dev/urandom
生成的資料可以說和真隨機完全無法區分,至少在現有科技水平下。使用比/dev/urandom
“更好的“隨機性毫無意義,除非你在使用極為罕見的“資訊理論安全”的加密演算法。這肯定不是你的情況,不然你早就說了。urandom 的 man 頁面多多少少有些誤導人,或者乾脆可以說是錯的——特別是當它說
/dev/urandom
會“用完熵”以及 “/dev/random
是更好的”那幾句話;
或者 Thomas Ptacek 的看法,他不設計密碼演算法或者密碼學系統,但他是一家名聲在外的安全咨詢公司的創始人,這家公司負責很多滲透和破解爛密碼學演算法的測試:
用 urandom。用 urandom。用 urandom。用 urandom。用 urandom。
/dev/urandom
不是完美的,問題分兩層:
在 Linux 上,不像 FreeBSD,/dev/urandom
永遠不阻塞。記得安全性取決於某個最一開始決定的隨機性?種子?
Linux 的 /dev/urandom
會很樂意給你吐點不怎麼隨機的亂數,甚至在核心有機會收集一丁點熵之前。什麼時候有這種情況?當你系統剛剛啟動的時候。
FreeBSD 的行為更正確點:/dev/random
和 /dev/urandom
是一樣的,在系統啟動的時候 /dev/random
會阻塞到有足夠的熵為止,然後它們都再也不阻塞了。
與此同時 Linux 實行了一個新的系統呼叫,最早由 OpenBSD 引入叫
getentrypy(2)
,在 Linux 下這個叫getrandom(2)
。這個系統呼叫有著上述正確的行為:阻塞到有足夠的熵為止,然後再也不阻塞了。當然,這是個系統呼叫,而不是一個位元組裝置(LCTT 譯註:不在/dev/
下),所以它在 shell 或者別的指令碼語言裡沒那麼容易獲取。這個系統呼叫 自 Linux 3.17 起存在。
在 Linux 上其實這個問題不太大,因為 Linux 發行版會在啟動的過程中儲存一點亂數(這發生在已經有一些熵之後,因為啟動程式不會在按下電源的一瞬間就開始執行)到一個種子檔案中,以便系統下次啟動的時候讀取。所以每次啟動的時候系統都會從上一次對談裡帶一點隨機性過來。
顯然這比不上在關機指令碼裡寫入一些隨機種子,因為這樣的顯然就有更多熵可以操作了。但這樣做顯而易見的好處就是它不用關心系統是不是正確關機了,比如可能你系統崩潰了。
而且這種做法在你真正第一次啟動系統的時候也沒法幫你隨機,不過好在 Linux 系統安裝程式一般會儲存一個種子檔案,所以基本上問題不大。
虛擬機器是另外一層問題。因為使用者喜歡克隆它們,或者恢復到某個之前的狀態。這種情況下那個種子檔案就幫不到你了。
但解決方案依然和用 /dev/random
沒關係,而是你應該正確的給每個克隆或者恢復的映象重新生成種子檔案。
別問,問就是用 /dev/urandom
!