学习笔记5

第9章 生成随机性

  为了生成密钥信息,我们需要随机数生成器(RNG)。生成良好的随机性是许多密码操作中非常重要的一部分,同时也是一项具有挑战性的工作。

  随机性的一个非正式定义是,即使攻击者采取主动攻击来破坏随机性,随机数据对于攻击者来说也是不可预测的。

  衡量随机性的度量称为,熵度量的不是随机数的位数,而是随机数的不确定性。对一个随机数而言,知道的信息越多,它的熵越小。

  随机变量\(X\)的熵的常用计算公式如下:

\[H(X):=-\sum_xP(X=x)log_2P(X=x) \]

9.1 真实随机

  在现实世界中,真实随机的数据非常难找到。

  有许多物理过程的行为都是随机的。比如说,在量子物理中,一些定律迫使某些行为是完全随机的。如果能够度量这个随机行为并使用它,是非常好的,并且这是在技术上行得通的。然而,攻击者对这种随机性有多个攻击方法。首先,攻击者可以尝试影响量子粒子的行为,使得它们的运动可以预测;另外,攻击者也可以尝试窃听我们所做的度量,一旦攻击者获得度量方法,尽管数据仍然是随机的,但是对攻击者而言,数据的熵值为零(如果攻击者知道该值,则该数据的熵为零)。

  一些现代计算机内置了真实随机数生成器,这相比于单独的真实随机数生成器有了显著提高,也使得一些攻击更困难了。随机数生成器仍然只能被操作系统访问,所以每个应用必须信任操作系统处理随机数据的方法是安全的。

9.1.1 使用真实随机数的问题

  真实的随机数除了难以获取之外,在实际应用中还存在其他问题。首先,随机数不是随时都可以获取的。比如说利用按键时长来获取随机数,那么只有用户按键时才能获得,这样在那些没有键盘的Web服务器上想这样获取随机数就是一个大问题了。另一个相关问题就是真实随机数的数量总是有限的,这对于许多需要大量随机数的应用来说是不可接受的。

  第二个问题就是真实的随机源有可能失效,比如说物理随机数生成器。一种失效的情况就是生成器的输出结果可以通过某种方式来预测。此外真实随机数生成器在计算机的噪声环境中是一个非常复杂的部件,它们相比于传统的部件更容易失效。如果直接依赖真实随机生成器,那么当这个生成器失效时就倒霉了。更糟糕的是,你可能不知道它什么时候会失效。

  第三个问题是如何判断从某一具体的物理事件中能够提取多少熵。除非你为随机数生成器设计了专门的硬件,否则就很难计算熵的值。

9.1.2 伪随机数

  伪随机数作为使用真实随机数的一种替代品,实际上并不是真正随机的。伪随机数的生成一般都是通过确定性算法,从种子计算得到。如果种子泄露,那么伪随机数就可以预测了。对于聪明的对手来说,传统的伪随机数生成器(PRNG)是不安全的,这是因为设计伪随机数生成器的初衷是消除统计上的缺陷,而不是为了对抗攻击者。

  在密码系统的上下文中,我们对伪随机数生成器还有更严格的要求。对于一个伪随机数生成器,如果攻击者即使掌握了它输出的随机数,也无法预测该生成器的其他输出结果,那么我们就称这样的伪随机数生成器是健壮的。

9.1.3 真实随机数和伪随机数生成器

  实际上,我们只需要将真实随机数作为伪随机数生成器的种子就可以解决问题。一旦伪随机数生成器获得了种子,随机数可以随时获取,同时我们可以不断获得新的真实随机数作为新的种子,这样即使种子泄露,生成器的输出也不可能被完全预测。

  理论上说,真实随机数比伪随机数生成器产生的伪随机数随机性更好。

9.2 伪随机数生成器的攻击模型

  对伪随机数生成器的攻击有很多形式。一种直接的攻击方法就是攻击者尝试从生成器的输出重构内部状态,这是一种典型的密码学攻击方法,使用密码技术也相对来说比较容易应对。

  攻击者也可以频繁地向伪随机数生成器要求获取随机数,由于两次请求之间所添加的熵总量是有限的,攻击者只需要获得随机输入的所有可能值,那么就能恢复出适量熵和原内部状态结合之后的新状态。

9.3 Fortuna

  Fortuna是一个伪随机数生成器,包括三个组成部分:生成器负责采用一个固定长度的种子,生成任意数量的伪随机数;累加器负责从不同的熵源收集熵再放入嫡池中,并间或地给生成器重新设定种子;最后,种子文件管理器负责保证即使在计算机刚刚启动时伪随机数生成器也能生成随机数。

9.4 生成器

  生成器负责将固定长度的内部状态转换为任意长度的输出。我们使用类似AES的分组密码作为生成器,也可以使用AES(Rijndael)、Serpent或者Twofish。生成器的内部状态包括一个256位的分组密码密钥和一个128位的计数器。

  生成器基本上可以认为是一个计数器模式的分组密码。如果用户或者应用请求随机数,那么生成器就会执行算法生成伪随机数据。假设攻击者打算在请求完成之后设法破解生成器的内部状态,我们就需要做到即使攻击成功,也不能泄露生成器之前输出的结果。因此,在处理完每次请求后,我们会额外生成一个256位的伪随机数据并把它作为分组密码的新密钥,然后清除旧密钥,这样就消除了所有泄露之前请求信息的可能性。

  为保证生成的随机数据满足统计上的随机性,我们不能同时生成太多的随机数据。这是因为真实的随机数据中可以存在重复的块值,但是计数器模式下输出的随机数中各块的值肯定不同。有许多方法可以解决这个问题,比如说每个密文块我们只使用一半,这样就会消除大部分的统计误差。我们也可以不使用分组密码,而是利用伪随机函数,但是目前并没有一个安全高效、经过详尽分析的伪随机函数能满足我们的要求。最简单的解决方法限制单个请求中生成的随机数据的字节数,这样就能使统计偏差更难检测。

  在每次请求结束后,分组密码的密钥会被重置,但是计数器不需要重置。

9.4.1 初始化

  将密钥和计数器设置为0,以表示生成器还没有获取种子。

9.4.2 更新种子

  更新种子操作通过输入任意字符串更新生成器状态。这时我们不用关心输入字符串的具体内容。为了保证输入字符串和现有密钥完全混合,我们使用了散列函数。

  计数器C被用作整数,后面它会被用作一个明文分组。

9.4.3 生成块

  这个内部函数的作用就是生成多个随机块,但只能被生成器调用。伪随机数生成器之外的任何模块都不能调用该函数。

  上面的代码中,\(E(K,C)\)是一个分组密码加密函数,密钥为\(K\),明文为\(C\)\(\varepsilon\)表示空字符串。GenerateBlocks函数首先判断\(C\)是否为0,当\(C\)等于0时就表示生成器还没有获得种子。接着以空字符串\(r\)开始一个循环,在循环中将每个新计算的随机块附加到\(r\)上以获得最终的输出。

9.4.4 生成随机数

  当用户请求随机数时,生成器就会调用这个函数来产生随机数据,输出长度最多可达\(2^{20}\)字节。同时这个函数还会确保之前生成的结果信息都已经被清除。

9.4.5 生成器速度

  Fortuna生成器运行的速度主要依赖于使用的分组密码算法,如果使用个人计算机上的CPU,那么处理大量随机数请求时,平均每产生1字节的输出需要花费的时间低于20个时钟周期。

9.5 累加器

  累加器负责从不同的熵源中获取实随机数,并用来更新生成器的种子。

9.5.1 熵源

  熵源的实现是一个复杂的工作,一般来说,熵源都会被嵌入到操作系统的各种硬件驱动器中,用户几乎不可能完成这项工作。

  为每个熵源指定唯一的熵源编号(熵源号),熵源号的取值范围从0到255。熵源号有静态分配和动态分配两种分配方式。熵源产生事件所包含的数据就是一连串字节,并且每个事件中都包含了不可预测的数据。比如,我们可以用精确计时器的2或4个最低有效字节来表示某个时间信息,时间数据中没必要包含年、月、日,因为攻击者获取它们非常容易。接下来,要将不同熵源产生的事件链接起来。为了保证链接后获得的字符串唯一标识这些事件,我们必须确保这个字符串是可解析的。每个事件都被编码为3个或更多的字节。第一个字节表示事件的随机熵源号,第二个字节表示数据的附加字节数。后续字节包括熵源提供的任何数据。

  当然,攻击者可能获取部分熵源产生的事件。为了模拟这个,假设攻击者控制了一些熵源,并且攻击者能够决定这些熵源在何时产生何种事件,而且和其他用户一样,攻击者任何时候都能够向伪随机数生成器请求随机数据。

9.5.2 熵池

  为了更新生成器的种子,需要将事件放入一个足够大的熵池中,“足够大”意味着攻击者无法穷举出熵池中事件的所有可能值。利用这样的熵池生成新的种子,可以破坏攻击者之前获得的关于生成器状态的信息。不幸的是,不知道在更新生成器的种子前应该在熵池中放入多少事件。

9.5.3 实现注意事项

  1. 基于熵池的事件分发
      熵源产生的事件需要分发到各个熵池中,最简单的解决方案就是让累加器负责这项工作,但是这样会有风险。如果累加器负责分发事件,那么就会有一个函数实现把事件交给累加器的功能,很有可能攻击者也可以对这个函数进行任意调用。攻击者可以在每次生成“真实的”事件后,对该函数进行额外地重复调用来影响下一次“真实的”事件会被放入哪个熵池中。如果攻击者能够使累加器将所有“真实的”事件都放入\(P_0\)中,那么整个多熵池系统的效率就会下降,并且可以使用针对单熵池系统的攻击方法实施攻击。如果攻击者使得所有的“真实的”事件都被放入\(P_{31}\)中,那么这些事件基本上永远不会被采用我们的解决方案是让每个事件发生器在产生事件的同时就确定事件将要放入的熵池号。

  如果攻击者还想影响事件被放入的熵池的选择,那么他就必须要有对生成事件的程序内存的访问权限,但是如果攻击者拥有这么大的权限,这个熵源也很可能已经被攻破了。

  1. 事件发送的运行时间

  当一个事件被发送给累加器的时候,我们希望能够对需要进行的计算量进行限制。这是因为许多随机事件都是定时事件,而这些事件是由实时驱动程序产生的,如果累加器处理某些事件需要花费很长时间,对于调用累加器的驱动程序显然是不适用的。

  但是,有少量计算是必须要完成的。必须将事件数据添加到指定的熵池中。当然我们不会将熵池中的整个字符串都存储在内存中,因为它们可能是无限长的。回想一下,常用的散列函数都是具有迭代性的。对于每个熵池,我们将分配一个缓冲区,当缓冲区满的时候,计算缓冲区包含内容的部分散列值。这是累加器处理每个事件时所需的最小计算量。

  使用一个或多个熵池来更新种子,所需要的时间是添加事件到熵池所需要时间的一个数量级以上,因此更新种子并不会作为累加器处理事件的必需运算,而是等到下次用户请求获取随机数据时、在随机数据生成之前才会进行。这样一来,部分计算负担就从事件生成器转移给请求伪随机数据的用户,这是合理的,因为用户也是伪随机数生成器服务的受益者。毕竟大部分事件生成器都不会从伪随机数生成器上受益。

  经过以上分析,可以选择扩大熵池的缓冲区,使得在计算散列值之前缓冲区中能够收集到更多的数据,优点是能够减少CPU计算总时间,缺点是将事件添加到熵池中所需要的最长时间变长,这需要在实现时根据具体的环境进行权衡。

9.5.4 初始化

  下面定义的函数实现了Fortuna的一些外部接口,是在整个伪随机数生成器上进行操作的。

9.5.5 获取随机数据

  该函数不只简单地对伪随机数生成器的生成器组件进行封装,还要处理更新种子。

9.5.6 添加事件

  当熵源产生了新的随机事件时,就会调用该函数。每个熵源都有唯一的熵源编号。

  事件\(e\)经过编码后长度为 \(2+length(e)\) 个字节,其中熵源号\(s\)\(length(e)\) 各占一个字节,接着将编码后的事件添加到熵池中。注意上面的代码只是将事件数据添加到熵池中,并没有涉及散列运算,因为这里假设只有熵池被使用时才会对其进行散列运算。在真正的实现中,我们应该一边向熵池中添加事件一边计算散列值,这在功能上是相同的,而且实现也更加容易,但是要进行详细描述就会十分复杂。

9.6 种子文件管理

  到目前为止,伪随机数生成器能够成功地收集熵,并且获取第一个种子后能够生成随机数。然而,每次计算机重启后,我们必须等到熵源提供足够的事件后,才能获取第一个种子来生成随机数据;另外,我们也无法保证第一次种子后的状态是攻击者无法预测的。

  解决方案就是使用种子文件。伪随机数生成器保存了一个熵文件,叫作种子文件,种子只有伪随机数生成器可以获取。每次重启之后,伪随机数生成器读取种子文件并使用文件内容作为熵源来获得一个未知状态。当然,种子文件在每次使用之后都需要重新写入内容。

9.6.1 写种子文件

生成种子文件。

  该函数生成64字节的随机数据,然后写入文件,64字节比实际需要的稍微多一点。

9.6.2 更新种子文件

  该函数的操作包括读取种子文件、检查种子长度和更新生成器的种子,接着用新数据更新种子文件。

9.6.3 读写种子文件的时间

  在计算机刚刚启动的时候,伪随机数生成器并没有熵来生成随机数据,这正是使用种子文件的原因,所以在每次启动后,都需要对种子文件进行读取和更新。

  计算机在运行的时候能够从各种熵源收集熵,这些熵最终也能够影响种子文件的内容。
  我们只需要保证伪随机数生成器在收集到相当数量的熵后能够定时更新种子文件,一种合理的解决方案是在每次关机时以及每10分钟左右对种子文件进行一次更新。

9.6.4 备份和虚拟机

  要保证能够正确地更新种子,还需要解决其他一些问题。因为不允许伪随机数生成器出现重复状态,所以使用种子文件。但是大部分存放种子文件的文件系统并不能保证避免出现重复状态,这就引出一些新的问题。

  首先是备份。在累加器收集到足够的熵之前,伪随机数生成器在两次重启之后生成的输出是相同的。这是一个严重的问题,因为攻击者也可以利用这种方法来获取用户从伪随机数生成器那里得到的随机数据。

  对于这种攻击没有直接的防御方法,最理想的是,我们修复备份系统使得它对伪随机数生成器敏感,但是这可能要求太多了。可以将种子文件的内容和当前时间进行散列之后再使用,前提是攻击者不会将时钟恢复到相同的时间。如果备份系统有一个计数器记录已经进行过多少次恢复,也可以将种子文件的内容和恢复计数器的值进行散列之后再使用。

  虚拟机上也有相似的问题,如果一个虚拟机状态被保存了,然后重启了两次,那么从同状态启动的两个实例,其伪随机数生成器的状态也是相同的。关于虚拟机的解决方案也可以参考备份的解决。

9.6.5 文件系统更新的原子性

  无论何时使用种子文件来更新种子,都要在用户请求随机数据之前更新种子文件,而且必须要保证磁盘上的数据也进行了更新。在一些文件系统中,对文件数据和文件管理信息的操作是分开进行的,所以更新种子文件时文件管理信息和实际的文件信息不符,如果此时机器断电了,种子文件就会损坏甚至丢失,这样的文件系统对安全系统来说是不适用的。

9.6.6 初次启动

  第一次使用伪随机数生成器时,没有种子文件可以用来设定种子。

  一个好的方法是,可以让安装过程在配置的过程中为伪随机数生成器生成一个随机种子文件。

9.7 选择随机元素

  设集合中元素个数为\(n\),现在讨论如何从 \({0,1,\cdots,n-1}\) 中随机选择一个元素。

  如果 \(n=0\) ,没有可以选择的元素,报错;如果 \(n=1\) ,只能选择唯一的一个元素,处理也比较简单;如果 \(n=2^k\) ,那么可以向伪随机数生成器请求一个\(k\)位的随机数据并转化为\(0\)\(n-1\)中的一个整数,这个数是均匀随机的。

  当\(n\)不为2的幂时,可以使用试错法。

  下面是在 \(0,\cdots,n-1(n\ge2)\) 中选取一个随机数的描述:

  1. \(k\)为满足 \(2^k\ge2\) 的最小整数。
  2. 利用伪随机数生成器生成一个\(k\)位的随机数\(K\),\(K\)的范围为 \(0,\cdots,2^k-1\)
  3. 如果 \(K\ge n\),返回第2步。
  4. \(K\)为最终结果。

第10章 素数

10.1 整除性与素数

  几个简单的引理:

  1. 如果\(a|b\)\(b|c\),那么\(a|c\)
  2. 如果\(n\)为大于1的正整数且\(d\)\(n\)除1之外最小的因子,那么\(d\)是素数。
  3. 素数有无穷多个。

10.2 产生小素数

  Eratosthenes筛选法:

10.3 素数的模运算

10.3.1 加法和减法

10.3.2 乘法

10.3.3 群和有限域

  称模素数\(p\)的集合为有限域,对于\(mod\; p\)域中的计算有如下一些性质:

  1. 对运算中的每个数,可以加上或减去\(p\)的任何倍数而不改变运算的结果。
  2. 所有运算的结果都在\(0,1,\cdots,p-1\)范围内。
  3. 可以在整数范围内做整个计算,只在最后一步做模运算。于是所有关于整数的代数规则仍然适用。

  群可以包含子群。一个子群由整个群中的一些元素组成,如果对子群中的两个元素进行群运算,得到的结果也要为子群中的元素。

  我们使用子群来加快一些密码运算,同时子群也可以用来攻击一个系统。

10.3.4 GCD算法

  两个数\(a\)\(b\)的最大公因子(或GCD)就是满足\(k|a\)\(k|b\)的最大整数\(k\)。换句话说,\(gcd(a,b)\)是能够同时整除\(a\)\(b\)的最大的数。

10.3.5 扩展欧几里得算法

10.3.6 模2运算

  模2加法就是编程语言中的异或函数(XOR),乘法是简单的AND运算。在模2域中,只有一个可能的逆(1/1=1),所以除法运算与乘法运算是相同的。

10.4 大素数

  产生一个大素数的过程如下:

  函数isPrime是一个两步过滤器。第一步是简单的测试,用所有的小素数来除\(n\),这样能够很快地剔除掉那些能够被小素数整除的合数。如果找不到因子,第二步就进行一个重要的测试,称为Rabin-Miller测试:

10.4.1 素性测试

  这里选择的素性测试是 Rabin-Miller测试,具体算法如下:

10.4.2 计算模指数

  使用快速指数幂来计算 \(a^s mod\; n\) ,步骤如下:

  1. 如果\(s=0\),那么结果为1。
  2. 如果\(s>0\)\(s\)为偶数,那么先使用这些规则计算\(y=a^{s/2}mod \; n\),结果为\(a^smod \; n = y^2 mod \; n\)
  3. 如果\(s>0\)\(s\)为奇数,那么先使用这些规则计算\(y=a^{(s-1)/2}mod \; n\),结果为\(a^smod \; n = a \cdot y^2 mod \; n\)
posted @ 2023-03-21 20:57  acacacac  阅读(48)  评论(0编辑  收藏  举报