==Phrack Inc.==
Volume 0x0b, Issue 0x3b, Phile #0x0f of 0x12
|=--------=[ CRYPTOGRAPHIC RANDOM NUMBER GENERATORS ]=--------=|
|=------------------------------------------------------------=|
|=----------=[Author: DrMungkee <
[email protected]> ]=--------=|
|=------------------------------------------------------------=|
|=----------=[Translator: winewind <
[email protected]>]=-------=|
密码学里的随机数发生器
----| 介绍
对于密码系统的安全性来说,每个组件都是很重要的。一个组件设计的失败可能使其他所有的组件崩溃。密码随机数常常被用作密钥,补充信息,辅助信息和初始化向量。对每一个组件来说,使用一个好的随机数发生器(RNG)是必要的。利用计算机行为的可预测性,可以人为的制造很多复杂因素。本文的范围涵括了随时数产生器的设计,执行和分析。将要涉及的随机数发生器(RNG)包括:NoiseSpunge, Intel 随机数发生器(Intel RNG),Linux下的“/dev/random”和Yarrow。
----| 关键词:
RNG - 随机数发生器(Random Number Generator)
PRNG - 伪随机数发生器(Pseudo Random Number Generator)
信息熵(entropy) - 不可预知信息(Unpredictable information)
冗余信息(redundancy)- 可预知信息(Predictable or probabilistic information)
(译者注:entropy一词源于物理,是“熵”的意思。在信息技术中引入,从而有了“信息熵”的说法。“熵”的特性:在封闭系统中,系统的熵只会增加或保持不变,系统的平衡点是熵最大的时候。无线电中常翻译成“平均信息量”。我也不知道这里用信息熵的说法是否便于理解。如果觉得别扭,就理解成一种信息好了)
(译者注:这里我加入一段引来的文章,可能会让大家对这个名词了解得更好:现代信息学的基础是信息熵理论,即对被传送信息进行度量的一种平均值,单位是比特。四十年代,现代信息论创始人、美国贝尔实验室科学家闪农(C.Shannon)发明了信息熵理论,由此提出了数据优化编码、输入输出效率、通讯传递渠道效率、多余度和数据压缩等一系列信息科学基础理论和技术。信息熵是信息产业的地基。比如,不管计算机硬件软件如何更新换代,英文的字符平均信息熵(静态信息熵)是4.03比特,因而,处理和储存英文数据的每个字符的编码不能少于5比特;中文的汉字平均信息熵是9.65比特,因而,处理和储存中文数据的每个字符的编码不能少于10比特。)
1.0) 概述
设计一个随机数发生器(RNG)需要考虑各种因素。在白噪音环境中(译者注:在一定带宽内,在各个频率上,各种噪音的强度都是一样的),输出必须是不可识别的。输出的任何一部分都是不可预知的。而且基于已知的结果,无法推算出上一步(不可逆)和下一步的结果。如果一个随机数产生器不能遵照这个标准,那么产生的密码是不安全的。
1.1) 信息熵(entropy)的收集:
为了满足第一条和第二条要求,为这些信息熵找寻好的来源(信息源)就成了一个首选任务。这些信息源对于攻击者必须是不可监测的。而且任何对信息源的操作对攻击者来说都是不可知和无法重复的。
鼠标的移动常常被用作信息熵的一种。但是如果RNG不能正确的处理信息熵,将会产生大量的冗余信息。举个例子,鼠标移动的时间间隔是100毫秒。当鼠标在各方向快速移动时,其位置记录如下:
X-轴 Y-轴
0000001011110101 0000000100101100 注意:在各个坐标中只
0000001000000001 0000000100001110 有最后九位是随机的。
0000001101011111 0000001001101001
0000001000100111 0000000111100100
0000001010101100 0000000011111110
0000000010000000 0000000111010011
0000001000111000 0000000100100111
0000000010001110 0000000100001111
0000000111010100 0000000011111000
0000000111100011 0000000100101010
下面的例子演示了一个更加实际的信息熵的收集过程。保留X-坐标和Y-坐标的最后四位(因为它们最重要),和采样得到的时间作异或运算,并把它们随意排列如下:
X Y 时间 异或后
1010 1001 00100110 01111111
0100 1100 00101010 00000110
0101 0010 01011111 01110101
1001 1100 10110000 11111100
0101 0100 11001110 11100010
0101 1100 01010000 01111100
1011 0000 01000100 00011100
0111 0111 00010111 00101000
0011 0101 01101011 01110110
0001 0001 11011000 11010001
这是一个好办法,因为从各坐标得到的四位数代表了在16像素平面上的任意方向(译者注:上面的数据表明,这个范围对纪录鼠标的运动已经足够大了),而不是 256*256平面上65536种移动方向。选用时间是因为虽然它们是连续的,但是它们的最后八位在CPU时钟周期内非常频繁的变化,这些比特位是无法人为预料的。异或运算用于合并两方面的信息熵。在合并数字并且保留各比特位的独立上,异或是个好办法:)
最常见的信息源包括用户的人为反应或某种经过排列变形后的高频时钟的序列。把上面两种合二为一得到的结果往往就是我们所需要的。用高精度采集用户触发事件的反应时间(击键,磁盘输入/输出,中断,鼠标点击)的方法有其优越性,因为用户个体行为和精确的时间是不可预知的。
有些信息看起来是足够随机的,但实际上未必。有时可以把网络的输运情况作为一种信息源,但我们并不推荐这样做,因为这种信息毕竟还是可以由某些外来因素控制的。另一个缺点是使用毫秒精度的时钟:对于比较高的要求,这种刷新频率显得的太慢了。
在讨论信息熵收集方法缺点方面,这里有一个不错的案例:Netscape公司使用的SSL加密协议是可破解,它并没有使用真正的RNG。(译者注:可以在下面的网址找到介绍 http://www.cs.berkeley.edu/~daw/papers/ddj-netscape.html)Netscape公司标志进程和父进程时使用时间和日期,并把它们当作唯一的信息源。在win9x中进程标志通常都是由一个小于100的值(每增加一个新进程就加一)和win9x第一次启动时的时间日期作异或运算得到。虽然由哈希函数得到的结果看起来是随机的,实际上猜测出那个值,计算并且得到正确的结果并不是件难事。如果可利用的信息熵非常有限,那么输出结果是不是真的随机也就不是很重要了(好像有点无奈的意思:P)。
1.2 信息熵的估算
我们不能忽视对收集到的信息熵总数的估算。这一步很重要。否则随机数产生器输出结果中信息熵的数量有可能超过输入的信息熵的数量。根据相应的系统参数,我们可以给各个信息源赋相应的值。比如,对于键盘活动,不管我们能够收集的信息熵总数是多少,我们都可以假设所有键盘活动产生的信息熵的大小都是4比特的。如果RNG在文件服务器上,并且把磁盘输入/输出作为信息源,我们可以根据某时刻正在访问磁盘的用户数相应的估计信息熵,而不是根据访问序列。如果基于后者,则可能产生多余的信息。对信息熵大小的估算不一定要和输入或输出的大小一样。这条准则在今后的计算中能起到安全预防的作用。
对信息熵的估算还有其他的方法。如果信息源超过一定时间间隔后还没有给我们提供数据,使用统计的方法计算偏差会获得更好的效果。我们可以从buffer收集大量的数据信息,经过压缩,得到压缩比的值。相对于之前输入的大量数据,统计测试表明,最后一次输入的数据对于检验当前输入数据整体属性起不到什么作用。但是对于RNG来说,则意味着可以把这些只能增加统计概率的输入数据舍弃。
最好的办法莫过于多试几次。在估算信息熵值时用一种方法往往是不够的。信息源的情况是复杂的,得到的信息熵也是多种多样的。可是在实际中,对所有的信息熵的大小往往赋同一个值。因此在做这种假设之前,必须仔细的分析一下。多尝试几个值是明智的选择。而最小的值往往是最准确的。
1.3) 信息熵的集合
没有任何信息源可以说是完美无缺的。确切点说,在计算机上是这样。这就是为什么我们在buffer(信息熵的集合)收集了信息之后还要进行一些处理。收集完毕后,信息熵就被输入到一个集合里。这个集合必须和输入有以下的关系:要知道包含元素的个数,把最后一次输入合并到之前收集的数据中并保持其一致性。另外,抛开那些收集到的信息熵的属性不论,集合还要为这些数据提供一个至少看起来随机的状态(相似的输出在这个集合里也应该是看起来随机的)。
对于某个集合状态,处理集合内容时(这里指把所有元素合并起来),既不能减少元素,也不能添加元素。如果经过某个合并函数的处理,集合变大了,那么必须保证这对信息熵的估算是没有影响的。只有那些负责收集信息熵的函数才能改变信息熵的大小。而且这些收集函数是分开作用的,彼此独立。
最好的合并函数是哈希算法(译者注:又称散列法)。哈希算法能够接受任意大小的输入,它的大范围输出可以反映信息熵合并的速度,并且这个算法的输出是不确定的。为了保护那些合并后的信息熵(译者注:保证没有信息丢失),哈希函数输入的大小不大于输出的。也就是说,如果哈希函数的输出是160位的,那么之前的输入最多160位。如果哈希函数用于密码处理上是安全的(事实上的确如此),那么输出的信息熵的个数应该和输入的一样。但是如果输出的多于输入,并不能认为信息熵集合里的态一定增加了。
处理大集合有以下几种途径:一种方法是把这个集合线性hash(杂化)。使用这种方法,你可能需要一个buffer保存最近的一次输入。hash从buffer的尾部开始,一次只hash一块(块的大小和输出的大小相同)。每次把当前输出和前一个块的hash结果作异或运算,以次保证整个集合只被最近的一次输入作用,而不会改写以前的结果。这只是一个例子。不管你选择什么样的处理方式,必须保证这种方式遵守前面所说的各条准则。
另一个处理大集合的方法是“multiple hash(多样杂化)”,使集合的内容互相影响。一个常见的用途就是用于处理包含“不可操作的信息熵”的集合。一旦这个集合满了,它就会被hash并用于更新另一个集合。更新的方式可以是更新hash的关系,也可以是直接作异或运算。这样尽可能多的集合就串联了起来。为了避免丢失已有的信息熵,一些集合只有在它们的父集合(更新这些集合的集合)被更新若干次(译者注:更新次数事先定义好的)后才能被操作。比如,只有当第一个hash集合被更新了8次后,下一个集合才能被更新。只有下一个集合被更新满了3次,它才能用于hash第三个集合。在这种方法里,第三个集合包含了经过24次hash的信息熵。这样保留下来的原始信息熵变少了(受杂化关系的限制)但是这些信息熵的性质却更好了。因为第三个集合里的信息源完全基于24次输入的信息熵。
向一个集合中输入信息熵往往称为更新或播种。这种信息熵的集合和基于它自身构建的输出函数实际上是一个PRNG。在收集信息熵的过程中获得的真正的随机种子才是得到RNG的原因。只要输入了一个好的信息熵,RNG就产生一个无界区域(没有输出模式)。这和PRNG正好相反。相对于RNG的无界区域,后者是从一个半确定的点开始,重复以前的所有输出,并且重复的顺序和RNG是一样的。
信息熵的集合对于防范攻击者推算RNG以前的输出和以后的输出结果至关重要。对RNG的攻击就是基于三部分:对信息熵集合性质的了解,信息熵的输入和以前的输出结果。要防止别人了解集合当前的状态,就要对以后的输出结果做一些处理。因此,集合必须不时地做一些大的变动,删除一部分或是全部的信息熵。这个过程叫做再播种。再播种,总是(译者注:用逗号隔开是为了强调),在输出之前用一些新的信息熵替换那些已经被删除的。如果没有上面的替换,这个集合的安全性就很成问题了。RNG不需要再播种,但是如果没有这步,就必须不停的添加信息熵的输入,添加的速度还要高于输出的。
并不是任何时候都要再播种的。只有当未用过的信息熵积累了足够多并且占了集合空间的一大部分时才使用再播种。要注意的是,对集合中信息熵的估算要随着输入数据的大小作相应的调整。再播种不能频繁的使用。是否使用它的唯一根据就是随机数生成器输出的比特位数和整个集合的大小。当集合里95%的信息熵都已经输出时采用再播种,这是一个比较适当的频率。这里我们假设信息熵的输入是在RNG输出的空隙间完成的。如果不是这样,那么使用再播种的次数有可能应该更多一些。在输出空隙间输入的信息熵越少,攻击者就越容易找到输出方式的蛛丝马迹,从而推算出上一次输出的结果(这样循环往复,一个链式的逆向推算就有可能成功地得到攻击者想要的东西)。(译者注:这里我们并不规定两次输出之间只能有一次输入。相反,输入的数据应该多于输出。这样,可以想象,在集合里用不到的数据会越来越多。等他们多到一定程度时,如上文的95%,一次性的替换掉。这就是一次的再播种。)
1.4)输出函数
RNG的输出函数必须是单向的。单向的意思是输入的数据经过函数处理可以得到输出,但是根据输出的数据无法计算出输入的原始数据(译者注:就是不可逆啦,饶舌)。单向的hash函数是一个非常好的选择。更复杂的方法是把集合中的一部分元素作为key data(译者注:不好意思,我一时还想不出什么好的词),使用对称加密算法,对另一部分加密,并且输出密文。压缩-解压缩也是一个效率很高的单向函数。为了达到目的,我们可以把集合中一部分元素当作PRNG的种子,生成多种输出(根据PRNG的种子大小而定)。然后把这些输出统统放进一个杂化函数得到结果。这样做保证了效率,因为处理时很多中间态(解压缩)hash的结果都是一样的,真正起作用的只是解压缩前的那个初始态。
RNG每次输出时,对它信息熵的估算都要随输出的大小而减少。这样做的前提是假设输出的数据都是由信息熵组成。由于输出的信息熵仍然保留在集合里,这些东西现在就成了冗余信息,也不该再把它们当作信息熵了(在集合里)。如果集合的大小是512位,且每次输出信息熵的大小是160位,那么三次输出之后,基本上所有的信息熵都被hash了,这个集合也就需要再播种了。
在处理信息熵集合时,有一个几乎不可能解决的问题:我们没办法确定信息熵的哪些位是要输出的,哪些不是。缓解这个问题最好的办法是:即便你看到了RNG的输出结果,我们也根本不让你知道哪些信息熵被用了几次。(译者注:看起来有点掩耳盗铃,但的确管用。我偷偷地用,用几次谁也不知道。感觉差不多了,我就再播种了。神不知鬼不觉:P)当一次输出完成后,集合的态发生变化,重新排序。这样,在既不添加信息熵也不再播种的情况下,RNG的输出结果也不会重复。集合的态的重新排序必须通过单向函数完成,而且输出函数必须满足前面提到的各条原则。只要处理的过程不违反前面的规定,我们认为集合里信息熵的大小在排序前后总是一致的。
1.5)执行
如果执行时不顺利的话,我们前面所作的所有努力都是白费。这里关于执行我们要谈三个方面的东西:媒质,硬件/软件和输出的使用。
在未加密状态,储存媒质和通信媒质各占了一个风险。下表列出了各种媒质的风险程度。对于风险程度的比例说明是这样的:
0 - no risk 无风险
1 - low risk 低风险
2 - medium risk 中等风险
3 - high risk 高风险
MEDIA (媒质) RISK(风险)
---------------------------------------------------
RAM <storage>(储存) 0 *&
Hard Drive <storage>(储存) 1 *&
Shared memory <transfer>(传输) 1 *&
Removable disks <transfer>(传输) 2
LAN <communication>(通讯) 2 &
WAN <communication>(通讯) 3
任何正确加密的媒质的风险都是0。
*如果储存媒质是在一台与网络相连的计算机上时,风险度还要加1。
&如果可以通过物理途径到达的话(computer/Lan),风险度也要加1。
所有媒质的最高风险都应该被解释成执行风险(向脆弱的机制说再见吧:)。高风险在实际中是不可接受的。媒质的风险程度是否可被接受取决于RNG的输出值(想想这在攻击者眼中的价值吧)。除非在你的壁橱里有许多的万能钥匙,否则一本私人日记就足以应付介质风险了(译者注:这里可能不太好了解。但就我的经历,国外用的日记本大多是带锁的。国内也有这种日记本,不过好像比较贵:P)。有关工业机密的一定要采用零风险的RNG。虽然什么样的风险是可以接受的往往取决于程序员,但是用户应该清楚地知道自己的选择。
硬件的RNG需要有监测能力。如果硬件发生了任何的物理修改,RNG就停止输出。这可以防止外界探测信息熵集合状态和输出。同时,外界无法通过频率,辐射,电压或者其他由RNG运行时产生的信息监控硬件的运转。一旦上述的任何一项可被探测,对于集合或输出来说都是一个危险。所以,所有的RNG硬件都要适当的屏蔽起来。
软件的执行就微妙的多了。防范逆向工程始终是个大问题,除非可执行文件发出的数字信号是在和操作系统一样的层次上执行的。否则,任何对逆向工程的防御措施只能是“缓兵之计”。所以,程序员一定要尽量减少软件的风险因素(上面的风险系数表对逆向工程依然适用)。
//下面提到的对RNG的应用和调用它的软件不同
RNG必须注意只有一个program可以得到自己的输出。数据传输的方式一定要是不可观测的。输出信息之间的差异由输出函数决定,但有时输出能被拷贝到一个临时的buffer。欺骗RNG在这个buffer中保存数据或者仅仅把数据拷贝到一些易于观测的地方,这是有可能做到的。一个简洁的解决办法就是:用一个程序自身生成一个key把RNG的输出结果加密。然而,无论你准备得如何充分,对于攻击者,程序总是有弱点的。程序员加入软件的防范措施只不过是个路障(并非不可克服的),但是足够抵挡99.9%的尝试攻击的用户了。对于剩下的0.1%,我们没什么办法了,因为理论上任何软件都是可以被破解的。
1.6)分析
我们常从两个重要的方面来分析RNG:随机性和安全性。评估一个RNG的随机性,我们可以分析输入(信息熵,收集过程)和输出(输出函数)的数据。评估安全性,我们要在信息熵的收集过程,集合,合并函数和输出函数中寻找漏洞,从任何可能的方面寻找那些能让攻击者得到过去,当前或以后的输出数据的漏洞。我们无法保证这些工作是多么的有效。唯一可以肯定的事是,一旦一个RNG被破解了,它就完了;否则,在此之前,我们所作的都是在投机。
因特网上有很多统计测试可用于检测数据的随机性。(译者注:我以前用过的一种方法是对所有的数据作傅利叶变换。Origin就可以作这件事。当然你也可以直接在一张坐标纸上画出所有的点,看看他们是否均匀,这显然是最粗糙的办法:P)大多数这些测试对数据量的要求都很大,把这些数据存在文件里,然后得到结果(译者注:从统计角度看,数据量越大,统计结果越接近于真实值。当然,这里面还有一些其他的要求,比如偏差什么的,不多说了)。统计分析的结果是一个概率值,用P表示。这是一个介于0和1之间的浮点型数据。作检测时,可以把数据块分成各种大小的,常见的是8位到32位。每次测试,P的精度都是不同的。通常我们期望P的值尽量接近0.5,在正中间,而不是偏向0和1的哪一端。但是如果这个值接近0或1,也并不说明这个RNG就是脆弱的。即使是纯粹的随机数据,这种情况也可能发生。但如果根本得不到接近1或0的值,对这个RNG来说总是个缺陷。想想看吧,如果数据是彻彻底底随机的话,所有的输出(各个数据统计值P)看起来就都差不多了。这就解释了为什么相似的输出是可能的。当P的值不尽如人意时,我们就需要另外取样并进行测试。如果别的样品得不到符合要求的P值,则这个RNG有可能有比较固定的输出,自然不能用它。还有一些测试会得到一个整数和一个期望值。整数离期望值越近越好。Maurer Universal Statistics Test就是一个这样的例子。(译者注:相关资料在 http://citeseer.nj.nec.com/maurer92universal.html上有很多)
统计测试的问题在于任何优秀的PRNG或哈希函数都能顺利地通过检测。尽管统计测试的结果并不总是有效的(辨别不出优秀的PRNG和哈希函数),但是想想,RNG也只不过是个RNG,我们只要它的输出能够不被预测就行了。(呵呵,外国人也讲黑猫白猫理论:P)统计的结果都不可靠,RNG的信息熵也是不确定的了。除非信息源能保证工作的很好,否则最好还是对原始的信息熵用同一种测试比较好。这样做随机性有足够的保障。然而,当你打算这样做的时候,一个大麻烦正横在你的面前。信息熵收集的步调通常都非常缓慢以至于收集到那些数据要花很长很长的极端枯燥的时间去等待,在某些情况下,这是不值得的。如何避免这种情况需要你仔细观察信息源的情况作出判断,而不是轻易的相信那些统计测试,这些测试不能保证什么,只是个参考(见1.1)。
评价一个RNG的安全性是一个复杂的工作,方法有无限多,结果却只有一个:破解。对于RNG来说,破解成败的可能性探讨起来很有趣。无论之前这个RNG抵挡住了多少次的破解进攻,总是会有新的攻击跑出来(一次的失败就已经足够了)。RNG的方方面面都要仔细研究,从信息熵的收集到输出结果的传输。每个组件都要单独测试,然后再组合起来调试。测试包括信息熵收集被hacked或控制的几率,还有对合并函数和输出函数的密码学分析。很多破绽都是在实验室条件下发现的。这叫做“学术破绽(academic breaks)”。让他们现出原形的条件通常都很特殊(一般情况下基本上是不可能的)(译者注:用我的话来说,就是“让他们现出原形的条件通常都很变态”:P)。对这些破绽的寻找是一个大课题,已经超出了本文的范围。成功的破解通常需要富有经验的密码专家的数月(经常是数年)时间的艰苦努力。最好的做法就是从开始设计RNG到完工始终把安全性的问题牢记脑中。
即使测试中已经极尽数学和密码分析学之能事,不过不要大意,科学的发展带来的新东西可能会给你的RNG带来新的破解方法。举个例子,Tempest Scanning可以用来监控击键和鼠标移动。甚至对白噪声(译者注:见前文解释)的分析也能带来新发现。那些学者和专业人士通常希望在破坏发生前尽他们的能力找出这些破绽。对于未知的攻击,我们做不了什么。但是开发者希望能找到快速有效的修复方法并从中汲取经验。幸运的是,这种攻击凤毛麟角,但是随着研究的深入,情况总是在变的。
我们只讨论在第二部分出现的那几种RNG,因为它们都经过了测试并通过了随机性分析。
----| 2 对几种RNG的讨论
2.1) NoiseSpunge的设计
源代码:活活,我已经给出了。
2.1.0)概述
NoiseSpunge是专门用来生成256位随机key的工具。它的加密十分坚固。为完成一次输出(256位),收集信息熵时需要用户移动鼠标几秒钟。其中的过程很复杂,而且运算量很大。当NoiseSpunge被选用于密码系统时,往往需要考虑一些特殊的实际情况。使用NoiseSpunge的缺点在于如果需要频繁的输出大量随机数据,它就显得有些笨拙了。因为用户要不停的移动鼠标,做出反应,而且它占用的CPU资源也较多。
2.1.1)NoiseSpunge对信息熵的收集
先给PRNG一个初始值为0的种子,用输出的值计算一个时间段的长度。每当时间一到,就检查鼠标当前的位置。时间是由计算机的高频时钟控制的。时间数据的最少4位有效数据和鼠标x,y轴坐标的至少4位有效数据作异或运算。然后,一个新的时间段又被给出。如此反复,直至32位信息熵都被收集完毕并且输出。32位数据输入到一个信息熵buffer中,同时用于更新设置时间的那个PRNG。这个过程被不断的重复。如果鼠标没有移动,新的时间段仍然会不断生成,上面的过程也不断循环直到发现鼠标移动。也有一种函数允许程序员一次性输入32位信息熵。如果信息源是硬件或在特定系统上能保证是安全的,这种函数还是很合适的。对于其他RNG的输出方法,在这种情况下,不是显得多余(如果管用的话),就与没什么用处(如果不管用)。
2.1.2)NoiseSpunge对信息熵的估算
信息熵的估算没什么复杂的地方。最麻烦的事就是一次次的输入。每次鼠标移动只能捕获4比特的数据。我们不需要再估算什么,因为它们只能产生4比特或更大一点的结果。有时候,一些辅助函数允许程序员添加一些自己的信息熵,这就需要程序员们保证他们的信息熵绝对是好用的;对这些输入的信息熵估算的工作,就只能靠他们手动完成了。
2.1.3)NoiseSpunge的信息熵集合
集合里的态包含762位(译者注:是不是768位。加法做错了?),包括一个256位的种子,一个256位初级杂化和一个256位的二级杂化。256位的HAVAL被用作杂化函数。当一个32位的信息块被添加时,它是被追加到一个256位的buffer中去的。一旦buffer满了,就用初次杂化更新它。种子一般是和初次杂化的结果作异或运算,除非这是第8次的初级再播种。(译者注:看看前面的例子,8次,3次,所以24次)再播种时,把初级杂化的输出当作输入数据作二级杂化。杂化结果改变了(见下面)并且替换种子。种子的改变是一个压缩-解压缩过程。32位的种子用于PRNG的随机种子,通常输出两个32位的数据。所有PRNG512位的输出都被杂化而且替换了集合的种子。每次初级再播种后,一个计数器就加1,直到8。这个计数器代表有多少个256位的信息熵已经加入了集合。计数器的作用在于粗略的判断什么时候不再需要添加信息熵,并暂停收集信息熵的线程(直到RNG输出)。
2.1.4)NoiseSpunge的输出函数
RNG的输出有两种方法:安全的和强制的。安全输出先判断计数器不为零,并递减,然后再输出。强制输出则不管计数器。输出时,种子被拷贝到一个暂时的buffer里,然后重排序。新种子被用于初始化Rijndael(块对称加密)。这个暂时的buffer用Rijndael加密并经过压缩-解压缩处理(和处理种子一样)。这样重复N次(N由程序员决定)后buffer输出数据。
2.1.5)NoiseSpunge分析
[1] 由于对鼠标移动过分依赖,一旦鼠标有一段时间不动,集合里的信息熵就会消耗殆尽。防治这种情况的办法是用一个计数器,当信息熵很少的时候就停止输出。
[2] 如果程序员强制的输入一些质量差的信息熵,这对RNG的内部状态会有损害。
[3] 没有高精度的计时器,NoiseSpunge就不会为系统工作。
[4] 即便集合的态是762(768?)位的,在任何态,信息熵的最大值都是256位。(其他的比特位其迷惑作用,用来掩护种子以防逆向工程的)。这样就决定了这个RNG只适用于一种情况:当随机数据的需求量较小时。
2.2)Intel RNG 的设计
信息来源:Intel 随机数发生器白皮书 *1 (Intel Random Number Generator White Paper *1)
2.2.0)Intel RNG 概述
Intel RNG使用的系统很多。它可以为任何软件提供超大量的优质随机数。它的平均“生产量”是75kb/s(比特)。Intel安全驱动在中间设备(CDSA, RSA-BSAFE, 和 Microsoft CryptoAPI)间架起了一座桥梁。这些中间设备负责把产生的随机数分发给提出请求的应用程序和硬件。硬件部分是Intel的810芯片,在将来的8xx芯片组里会集成到82802 Firmware Hub Device中。
Intel把它的RNG标榜成TRNG(True Random Number Generator 真正的随机数发生器),但是在白皮书的后面却继续称之为RNG。从技术上说,把它跟其他优秀的RNG之间并没有什么根本的区别。这是个骗人的噱头,和RNG生成随机数的能力根本无关(RNG==TRNG & TRNG==RNG)。至于你看到的法人担保:“Intel RNG的输出得到密码研究有限公司(CRI)的承认,并且通过了联邦信息处理中心(FIPS)关于随机数的第三级别统计测试(FIPS 140-1)。”对这个产品我比较放心,因为有个公司(CRI)专门对它做过分析并且支持这个产品。这种情况是不多见的。另一方面,FIPS140-1只是另一个骗人的东西。读完了它,你会发现它和我们想知道的RNG的性能根本没有任何关系。但是,嗨,管它呢!微软觉得这个东西不错,把它用于他们的“高质量安全产品”系列(their family of _high_quality_and_security_ products)中,所以应该是很不错了。言归正传,抛开浮夸的法人担保不说,这个产品的确设计得很好。它没有其他很多RNG都范那些的错误,就像Netscape的那个。我认为这是在正确方向上迈出的一步。Joe( Timmy的表弟),还有Timmy最好的朋友的朋友,他们没有像Netscape那样闭门造车,而是合作起来为每个人提供了一个好的解决方案。
2.2.1)Intel RNG的信息熵的收集
Intel的随机数发生器将要被整合到PC机的主板里,用到两个电阻和两个振荡器(一个比较慢,另一个比较快)。两个电阻之间的电压差被放大用来对热噪声采样。噪声源被用来调节慢的时钟。这个时钟有多种调节选择。它的用途是给另一个较快的时钟设置测量时间段的。当这个时间段被触发,较快时钟的频率就被输入一个Intel称作von Neumann corrector(尚未获得专利)的程序。这个过程像被过滤一般。这个程序为了补偿较快时钟的误差(这个时钟喜欢停滞在固定的比特状态),它通过比较一对对的比特位,只输出一个或不输出([1,0]=0; [0,1]=1; [0,0]或[1,1]=不输出)。这个corrector的输出是32位的数据块,并且这些块是送给Intel安全驱动的。
2.2.2)Intel RNG信息熵的估算
这里对收集过程不做什么估算。因为信息源是基于硬件的,如果工作环境的温度和正常值没有偏离得过分,或者电源被断开(这种情况下信息熵的收集停止了),这个信息源是不可人为控制的。除了以上的特殊情况,所有的信息熵的收集过程都是一样的,而且我们认为他们的性质没有差别。
2.2.3)Intel RNG的信息熵集合
Intel安全驱动负责看护合并RNG输出的工作。集合由512位的SHA-1(一种安全杂化算法)杂化关系组成。这个杂化关系分为两个状态。第一状态的80位的杂化生成后,在后面追加上32位的信息熵(从硬件得到)。再由第一状态的前160位生成第二状态。当另一个32位的信息熵产生后,把第二状态标志成第一状态,然后重复前面的过程。
2.2.4)Intel RNG的输出函数
第一状态的80位杂化的最后16位输出到中间设备。 Intel安全驱动保证每次输出只有一个目的地,不会分发到多个地方。如果需要,发出申请随机数请求的程序要对输出作些额外的处理。
2.2.5)Intel RNG 分析
[1] 需要执行von Neumann修正程序,因为RNG和循环次序有密切关系。一个攻击者可能通过估算RNG的“生产量”计算出什么时候1s或0s是不成比例的输出。但是这还不足以形成什么威胁。
[2] 基于协议的中间设备的使用可能会导致一些漏洞。在开始正式使用一个公司的中间设备之前,你可能需要花几个月的工夫看看使用中会不会有一些很短暂的停顿。
2.3)Linux的 /dev/random 的设计
2.3.0)/dev/random 概述
Linux把/dev/random作为一个接口提供给那些需要优质随机数的应用程序。它提供了一个大容量的信息熵集合(512比特)以满足操作系统和各种软件。当信息熵的性质不太重要时,另一个设备/dev/urandom可以用作PRNG,提供伪随机数,免得耗费/dev/random里的信息熵集合。
2.3.1)/dev/random 的信息熵收集
内核提供的外部函数负责触发向集合里添加信息熵这个事件。触发事件可以是按键,鼠标移动,也可以是IRQ中断。对于每次触发,32位的高频时间被复制,同时生成另一个32位的数据。后一个32位的数据基于某种触发的类型(鼠标状态,键盘的扫描代码(译者注:the data sent by the keyboard is called a scancode, 我只会用英文解释,中文该怎么说?),或IRQ代号)。
2.3.2)/dev/random 信息熵的估算
信息熵的估算基于三个Δ(译者注:就是那个读作delta的东西啦)。Δ1表示自从上一次触发后所经过的时间。Δ2表示现在的Δ1和以前的Δ1的差值(Δ2=Δ1[this time]-Δ1[last time])。Δ3表示现在的Δ2和以前的Δ2的差值(Δ3=Δ2[this time]-Δ2[last time])。Δ的值取Δ1,Δ2和Δ3中的最小值(Δ=min(Δ1,Δ2,Δ3))。这样我们舍弃了Δ中不重要的部分,剩下来的12位数据用来增大信息熵计数器的值。
2.3.3)/dev/random的信息熵集合
这个RNG使用一个4096比特的集合。在输入之前,做一个标记表示当前在集合里的位置要减去2个32位的数据。如果当前位置为0,这个位置就退回到倒数第二个32比特的地方。信息熵总是同时增加两个32位的:x和y。变量j表示有多少位是信息熵要循环左移的。在信息熵增加前,j要减去14(当集合当前位置是0时,j减7)。信息熵根据j的值改变。根据当前的位置,y和集合里其他5个固定的部分作异或运算(接下来的位置就是从当前位置开始封装:103,76,51,25,1)(对于4096位的集合),同时x分别和后面的字作异或运算。x右移3位,再和一个1x7的表格里的中一个常数作异或运算(0,
0x3b6e20c8, 0x76dc4190, 0x4db26158, 0xedb88320, 0xd6d6a3e8, 0x9b64c2b0,0xa00ae278)。具体和哪一个异或则由x和7的与运算结果(3比特)决定。然后把x和y作异或运算,把结果跳过一个字追加到集合里。y右移3位,和前面x一样和一个常数异或,并把结果拷贝到刚才跳过的那个字里。(译者注:这里说的“字”指得都是32位的。我是不是该用“双字”这个词?)集合保存这个位置(以前的位置减2,可能在集合的最后)。
2.3.4)/dev/random的输出函数
当RNG需要输出时,计时器的值和那些比特位被当作信息熵输入到集合里。然后集合和SHA-1杂化,杂化结果的前两个字作为信息熵输入到集合里。这个步骤重复了8次,但是每次杂化结果的第三个和第四个字也被输入到集合里。最后的杂化结果的前半段和后半段再作异或运算,这个结果才是输出结果(译者注:快晕了~~基本上整个集合都换了吧)。结果的大小不是请求的大小就是20比特(杂化结果的一半),取这两者中较小的一个。
2.3.5)Linux' /dev/random分析
[1] 在网络环境下对一些IRQ的控制和预测是有可能实现的。
[2] 在增加的信息熵的低16位里会有一些冗余信息。举个例子,当按键产生一个32位的变量,其中16位来自于高精度的计时器,低16位则是按键的产生的0-255(256以上的是用于设计中断的)。这样每次按键都会产生8比特的冗余信息。
[3] 上一次信息熵数据块输入后的时间通常和整个信息熵的性质没什么关系,除非这个时间非常短暂。这和移动文件时持续访问磁盘不一样,信息熵序列不是连续的。
[4] 输出时,合并机制重新输入杂化后的信息熵,这些数据的性质可能好,也可能不好。这些重新输入的数据被添加到信息熵的计算里,而实际上不该这么做。它们已经被计算过一次了。输出后,512位的信息熵作为冗余信息被添加了进来。如果估算的没错的话,输出8次后,集合里有4096比特未知性质的信息熵(整个集合里)。在这种情况下,如果没有来自基于用户反应的输入的话,RNG就成了PRNG。
2.4)Yarrow的设计
信息来源:Yarrow源码和白皮书 *3,*4。
2.4.0)Yarrow概述
Yarrow的设计者是Bruce Schneier。他是《应用密码学》的作者和Blowfish块状密码及AES 入围决赛的 Twofish 的设计者。Yarrow是Schneier讲解如何设计一个好的RNG时用的例子,并有一篇详细的文章介绍它的内在工作机制和分析(见信息来源中的第二篇)。这个产品是长期研究的成果,并对RNG的安全性提出了一些标准。列在这里是为了让大家比较比较市场上流行的RNG和一个经验丰富的教授设计出来的RNG有什么不同。
2.4.1)Yarrow的信息熵收集
用系统钩子等待键盘或鼠标的事件。如果一个键被按下,从上一次按键到现在经过的时间被添加到一个数组里。鼠标按键的情况和这个一样。如果鼠标移动了,x轴和y轴的坐标也会被添加到一个标志鼠标移动的数组中。一旦这个数组满了,就会移交给信息熵的估算函数。
2.4.2)Yarrow对信息熵的估算
信息熵的估算函数处理的数据是由程序员通过对信息源的理解自己选出的。比如,你可以定义一次鼠标移动的信息熵是4比特,每次按键的键盘延迟是8比特。另一种测量方法是采用一个小的压缩算法并测出压缩后的大小。第三种方法是直接取信息熵采样得到的数据大小的一半。这三种测量方法得到的最小值作为信息熵大小估算的结果。
2.4.3)Yarrow的信息熵集合
信息熵被输入到一个“快”集合里(由SHA-1算法得到),信息熵的估算被这个集合更新。一旦“快”集合收集满了100比特的信息熵,杂化输出被送入一个“慢”集合,信息熵的估算也被同时更新。当这个“慢”集合收集满了160比特的信息熵,它的杂化输出才是最后我们用到的key。
2.4.4)Yarrow的输出函数
输出时,用那个key(由“慢”集合生成的)加密一个计数器(它的比特数是由程序员选定的)并且输出密文;计数器随之加1。10次输出后,RNG对key再播种,用另一个输出(强制的)替换它。那个“慢”集合收集满160比特数据或10次输出完成都会引起key的再播种。
2.4.5)Yarrow分析
[1] 鼠标自身的运动是很繁杂的。当操作系统传递鼠标位置的信息时,鼠标产生的新位移是非常有限的。标志鼠标移动的数据中很多位都不会改变,这对RNG中信息熵的估算会产生一些不良影响。(译者注:想想文章开头举到过的那个例子)
[2] 即使集合内部的态是320+n+k比特那么长,任何态上可容纳的信息熵最多为160比特。“Yarrow-160,我们现在的产品,从安全角度考虑,它的信息熵集合大小规定不超过160比特。”*4
----|3)NoiseSpunge源代码
下面的源代码只是一个简单的例子。你可以对它作任何事;甚至是*@#@!#!#(译者注:原文这里写得比较xxx,但愿是因为我的水平太次:P)......没关系的。这个源代码是不完整的,因为我已经省略了包含Haval, Rijndael 和 PRNG的1200行代码。Haval 和 Rijndael的源代码很好找。另外,任何的PRNG都可以用在这里,只要你注意它的输入和输出是32比特的并且范围是2^32(4294967296)就行了。我把它分成了三部分:信息熵的收集,信息熵集合,还有输出函数。
[信息熵的收集]
分配给这个循环的线程必需和应用程序的主线程互相独立。考虑到移植性,我写了一个哑元函数以供替换:
int64 CounterFreq; //高精度计数器的频率 /秒
int64 QueryCounter; //高精度计数器的当前值
Delay(int ms); // 一毫秒精度的延迟
int GetMouseX; //当前鼠标x轴坐标
int GetMouseY; //当前鼠标y轴 坐标
#define MOUSE_INTERVAL 10
{
Prng_CTX PCtx;
int x,y;
unsigned long Block;
unsigned long BitsGathered;
int65 Interval,Frequency,ThisTime,LastTime;
unsigned long BitsGathered=0;
bool Idled=false;
Frequency=CounterFreq;
bool Terminated=false; //循环结束时设为真
do
{
if (Idled==false)
{
Delay(MOUSE_INTERVAL);
Idled=true;
}
ThisTime=QueryCounter;
if ((ThisTime-LastTime)>Interval)
{
if ((x!=GetMouseX)&&(y!=GetMouseY)
{
x=mouse.cursorpos.x;
y=mouse.cursorpos.y;
Block|=((x^y^ThisTime)& 15)<<BitsGathered;
BitsGathered+=4;
if (BitsGathered==32)
{
PrngInit(&PCtx,Block);
AddEntropy(Block); //这个函数在下面有定义
Block=0;
BitsGathered=0;
}
Interval=((((Prng(@PCtx)%MOUSE_INTERVAL)>>2)+MOUSE_INTERVAL)
* Frequency)/1000;
}
LastTime=QueryCounter;
Idled=false;
}
} while (Terminated==false);
}
[信息熵集合]
#define SEED_SIZE 8
#define PRIMARY_RESEED 8
#define SECONDARY_RESEED 8
//参数
#define MAX_KEY_RESERVE 8
#define KEY_BUILD_ROUNDS 16
typedef unsigned long Key256[SEED_SIZE];
Key256 Seed;
Key256 EntropyBuffer;
Haval_CTX PrimaryPool;
Haval_CTX SecondaryPool;
unsigned char PrimaryReseedCount;
unsigned char EntropyCount;
unsigned char KeyReserve;
//函数
void NoiseSpungeInit
{
HavalInit(&PrimaryPool);
HavalInit(&SecondaryPool);
for (int i=0;i<8;i++) Seed[i]=0;
EntropyCount=0;
PrimaryReseedCount=0;
KeyReserve=0;
}
void PermuteSeed
{
Key256 TempBuffer[2];
Prng_CTX PCtx;
Haval_CTX HCtx;
for (int i=0;i<SEED_SIZE;i++) //解压缩
{
PrngInit(&PCtx,Seed[i]);
TempBuffer[0][i]=Prng(&PCtx);
TempBuffer[1][i]=Prng(&PCtx);
}
HavalInit(&HCtx);
HavalUpdate(&HCtx,&TempBuffer,64); //压缩
HavalOutput(&HCtx,&Seed);
}
void PrimaryReseed
{
Key256 TempSeed;
HavalUpdate(&PrimaryPool,&EntropyBuffer,32);
if (PrimaryReseedCount<SECONDARY_RESEED)
{
HavalOutput(&PrimaryPool,&TempSeed);
for (int i=0;i<SEED_SIZE;i++) Seed[i]^=TempSeed[i];
PrimaryReseedCount++;
} else SecondaryReseed;
for (int i=0;i<SEED_SIZE;i++) EntropyBuffer[i]=0;
if (KeyReserve<MAX_KEY_RESERVE) KeyReserve++;
EntropyCount=0;
}
void SecondaryReseed
{
HavalOutput(&PrimaryPool,&Seed);
HavalUpdate(&SecondaryPool,&Seed,32);
HavalOutput(&SecondaryPool,&Seed);
PermuteSeed;
HavalInit(&PrimaryPool);
PrimaryReseedCount=0;
}
void AddEntropy(unsigned long Block)
{
EntropyBuffer[EntropyCount++]=Block;
if (EntropyCount==PRIMARY_RESEED) PrimaryReseed;
}
[OUTPUT FUNCTIONS]
int SafeGetKey(Key256 *Key)
{
Key256 TempSeed;
Key256 TempBuffer[2];
Rijndael_CTX RCtx;
Prng_CTX PCtx;
Haval_CTX HCtx;
if (KeyReserve==0) Return 0;
for (int i=0;i<SEED_SIZE;i++) TempSeed[i]=Seed[i];
PermuteSeed;
RijndaelInit(&RCtx,&Seed);
for (int i=0;i<KEY_BUILD_ROUNDS;i++)
{
RijndaelEncrypt(&RCtx,&TempSeed[0]); //加密
RijndaelEncrypt(&RCtx,&TempSeed[4]);
for (int j=0;j<SEED_SIZE;j++) //解压缩
{
PrngInit(&pctx,TempSeed[j]);
TempBuffer[0,j]=Prng(&PCtx);
TempBuffer[1,j]=Prng(&PCtx);
}
HavalInit(&HCtx);
HavalUpdate(&HCtx,&TempBuffer,64);
HavalOutput(&HCtx,&TempSeed);
}
for (int i=0;i<SEED_SIZE;i++) Key[i]=TempSeed[i];
if (KeyReserve>0) KeyReserve--;
Return 1;
}
void ForcedGetKey(Key256 *Key)
{
Key256 TempSeed;
Key256 TempBuffer[2];
Rijndael_CTX RCtx;
Prng_CTX PCtx;
Haval_CTX HCtx;
for (int i=0;i<SEED_SIZE;i++) TempSeed[i]=Seed[i];
PermuteSeed;
RijndaelInit(&RCtx,&Seed);
for (int i=0;i<KEY_BUILD_ROUNDS;i++)
{
RijndaelEncrypt(&RCtx,&TempSeed[0]); //加密
RijndaelEncrypt(&RCtx,&TempSeed[4]);
for (int j=0;j<SEED_SIZE;j++) //解压缩
{
PrngInit(&pctx,TempSeed[j]);
TempBuffer[0,j]=Prng(&PCtx);
TempBuffer[1,j]=Prng(&PCtx);
}
HavalInit(&HCtx);
HavalUpdate(&HCtx,&TempBuffer,64);
HavalOutput(&HCtx,&TempSeed);
}
for (int i=0;i<SEED_SIZE;i++) Key[i]=TempSeed[i];
if (KeyReserve>0) KeyReserve--;
}
----| 4) 参考文献
*1 Intel 随机数发生器白皮书
http://developer.intel.com/design/security/rng/CRIwp.htm
*2 /dev/random 源代码
http://www.openpgp.net/random/
*3 Yarrow 源代码
http://www.counterpane.com/Yarrow0.8.71.zip
*4 Yarrow-160:关于Yarrow密码伪随机数发生器的设计和分析笔记
http://www.counterpane.com/yarrow-notes.html
----| 5)最后的话
首先,感谢您读完了这篇文章。我知道,只是一篇偏于理论性的文章,对它感兴趣的人也许不会太多。一些心得我都已经夹杂在行文中了,希望对大家理解有些帮助。如果您对这篇文章的理解有些疑问或困难,我先道歉了。我的翻译水平和对RNG知识掌握的有限也许反而会增加您的理解负担。我推荐您看一看原文,虽然里面有不少的拼写错误......
认真读完这篇文章的人可能都会有一个感觉:设计一个好的RNG,从头至尾都不能有一点点的松懈。任何马虎都有可能给攻击者以机会,使自己蒙受损失。关于技术方面的东西,我也给出了一些参考资料,其中大部分是论文。这个课题无论从广度还是深度都是很有挖掘潜力的。如果您读完了这篇文章,对里面的技术细节还没有充分了解(我读了好几遍才有了点头绪),不要紧,首先记住一点:安全第一。
里面谈论技术时都比较繁杂,尽量用自己的理解看问题,多想想,不要被牵着走,常常把现在所说的问题和前面提到的例子结合起来看。这样也许对理解本文有些好处。
最后,谨以此文献给cherry。虽然我知道你对这个不感兴趣,但你至少没有反对我做这件事:)