Random Number Generator

来自osdev
Zhang3讨论 | 贡献2022年2月28日 (一) 06:36的版本 (创建页面,内容为“随机数生成器(RNG)可以用很多不同的方式实现。 本文解释了其中一些方式。 ==熵(Entropy)== 计算机是确定性设备。 如果程序相同且所有输入相同,则每次计算的结果都相同。 那么,计算机如何生成随机数呢? 计算机不可以是随机的,但它周围的物理世界可以。 许多物理事件在某种程度上是随机的,或者更严格地说,具有某种程度的熵。 即使在…”)
(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)
跳到导航 跳到搜索

随机数生成器(RNG)可以用很多不同的方式实现。 本文解释了其中一些方式。


熵(Entropy)

计算机是确定性设备。 如果程序相同且所有输入相同,则每次计算的结果都相同。 那么,计算机如何生成随机数呢? 计算机不可以是随机的,但它周围的物理世界可以。 许多物理事件在某种程度上是随机的,或者更严格地说,具有某种程度的熵。 即使在地球上最好的屏蔽录音室里,录音也会包含一定程度的噪音。 人类在键盘上输入的时间不一致,并且输入准随机文本,比如你正在阅读的文章。 甚至其他计算机也可以提供熵:虽然大多数网络流量完全是机器生成的,但最终网络数据包的确切相对时间来自按下电源按钮的那一刻,这是另一种人类行为。 因此,输入事件的时间和性质可以为我们提供一些熵。

随机数生成器的类型

有多种方法可以划分随机数生成器;一种是通过数字的生成方式,另一种是通过数字的使用方式。 这些是重要的区别:前者可能会影响操作系统的设计,后者可能会影响系统的安全性。

随机数的来源

有两种随机数:“真”随机数和伪随机数。

对于“真”随机数生成,系统持续测量预期为随机的特定事件集。 这可以是宇宙辐射和原子衰变,也可以是用户输入的时间和时钟抖动。 测量是去偏(de-biased)的,并“搅拌”到一个熵池中,从中可以提取随机数。

伪随机数由一种算法(PRNG)生成,该算法转换一些内部状态,并根据请求计算输出值。 可以设置初始种子,但在此之后,下一个状态仅取决于前一个状态。 有许多不同的PRNG,下面讨论其中一些。

可以使用一些“真”随机数来为伪随机生成器的状态设定种子,但这并不能使PRNG“真随机”。 根据具体的算法,预测所有下一个输出可能与预测前一个输出一样简单。 正如下一节所讨论的,这可能会产生严重的影响。

随机数的应用

大致有两种应用:密码学和其他方面。

为了理解以下部分,我们首先应该回顾一下关于随机数生成器的加密安全意味着什么。 密码学家将安全分为两类:信息论安全和计算安全。 前者是更有力的主张。 如果密码系统在理论上是安全的,那么攻击者在不拥有密钥的情况下,就不能得出任何关于加密消息的信息。 这种密码系统很少见。 由于攻击者不能得出“任何东西”,因此必须隐藏所有内容,包括模式、时间甚至长度。 一个人怎么能隐藏信息的长度呢? 唯一的选择是根本不发送消息,这违背了密码系统的目的,或者发送无限长的消息。 所以一旦你开始发送,你就永远不会停止。

这就是为什么计算安全性较弱,但更实用的一种:密文必须很难找到,“给定一些合理的计算能力”。 选择这一限制的目的是希望在未来几十年内不会达到。 这还允许用户“泄露”有关密文的一些信息(例如,长度),只要找到原始输入仍然困难。 这些系统通常使用一些很难反转的操作,除非已知密钥,例如大素数分解。 很明显,密钥不能是可预测的:如果对手知道下一个输出是什么,他们很容易猜测密钥,直到找到正确的密钥。

“真”随机数是加密安全随机性的最佳来源,但是很难建立一个大的、快速补充的熵池,其中每个源都是完全无偏的,并且不受其他源的影响。 要以难以逆转的方式“拉伸”熵,可以使用加密安全随机数生成器(CSPRNG)。 CSPRNGs保证,在看到之前的结果后,在计算上很难猜测下一个输出,如果生成器的状态已知,哪些值在已知输出之前。

真随机数生成器

真正的随机数生成器使用物理设备或现象生成随机数,其不可预测性可以追溯到量子力学定律。

x86 RDSEED指令

较新的x86和x86-64处理器具有用于生成随机数的指令RDSEED。 要使用RDSEED,首先需要检查指令是否可用。

mov eax, 7     ; set EAX to request function 7
mov ecx, 0     ; set ECX to request subfunction 0
cpuid
shr ebx, 18    ; the result we want is in EBX...
and ebx, 1     ; ...test for the flag of interest...

如果EBX设置为1(或ZF未设置),则该指令可用。 然后可以使用它生成16/32/64位随机数(取决于用作参数的寄存器大小)。 如果生成了随机数,则设置进位标志。

mov ecx, 100   ; number of retries
.retry:
    rdseed eax
    jc .done      ; carry flag is set on success
    loop .retry
.fail:
    ; no random number available
.done:
    ; random number is in EAX

这取决于你和你的用户是否信任RDSEED。 因为它是在硬件中实现的,所以它实际上是一个黑匣子,可能包含各种错误,或者更糟的是,后门。

专用硬件

有专门用于生成“真”随机数的设备。 从消费者级TPMs到PCIe“加密加速器”都有。 这些是RDSEED/RDRAND的一种泛化,缺点是需要额外的驱动程序来与设备交互,用户可能没有安装这样的设备。

TPMs或Trusted Platform Module是可以安装在现代主板上的小型协处理器。 除了生成随机数,它们还提供其他可信计算服务。 值得注意的是,Windows 11要求有TPM。 它们也可以在CPU上模拟(例如英特尔PTT或AMD fTPM)。 与RDSEED一样,后门可能是一个问题。

手动采样

在一个运行的系统中,有很多进程实际上是随机的。 想想每秒钟都会出现的各种中断:精度为百万分之几的计时器、外围I/O、用户与整个系统的交互等等。

挑战在于找到(矛盾的是)可靠随机且难以从外部影响和观察的来源。 对于这些源中的每一个,必须估计它们贡献了多少熵。 测量将各自的熵量添加到池中,而读取则会降低熵。

因为你可以完全控制这种生成方法,所以还可以合并硬件生成器生成的值。 你可以自己决定这些生成器的熵是多少,甚至是0位。

当使用计时作为熵源时,读取的时间戳应尽可能精确。 TSC在这方面做得很好。 衡量从该操作中获得的熵需要了解事件发生的时间窗口和TSC的滴答率。 例如,如果一个TSC的滴答频率为3 GHz,并且一个事件有一个10毫秒的窗口要发生,那么TSC读取可以有3000万个值中的任何一个,这意味着从中获得的熵约为24.8位。 如果TSC较慢,只有1GHz,那么熵将只有约23.2位。

对抗熵

如果对手能够以某种方式观察熵池的状态,并将自己的熵贡献给熵池,那么他们提供熵的方式可能会迫使熵池进入较低的熵状态。 一个简单的例子是,熵源定期检查池中的某个位是否已设置,然后提供熵,直到它被清除。 这样,在大多数情况下,给定的位是清晰的,熵缓冲区可能处于的状态数减少了一半。 DJB的博客上描述了一种更复杂的攻击:https://blog.cr.yp.to/20140205-entropy.html

部分原因还在于,如果用户请求一个随机数,不经修改地公开熵池是不明智的。 如果对手可以访问该池(通过专用的“添加熵”接口或采样事件源),则很容易对其下毒。 用于隐藏确切状态的常用方法是使用加密安全的哈希函数(如SHA-256),结合计数器(例如熵计数器)和salt对池(部分)进行哈希运算。 因为这些散列算法很难反转,所以它的输入不容易猜测。 只有当池中还有一些熵时,才需要这样做。

加密安全的伪随机数生成器(Cryptographically secure pseudorandom number generators)

现在跟随一些CSPRNGs。 重要的是要记住,与任何加密一样,如果你打算实际使用它,最好不要自制它。 出错的方式很多,算法越复杂,出错的几率就越大。 当然,对于业余用途来说,这是完全好的;只是不要使用手工制作的TLS密钥源进行网上银行业务。

x86 RDRAND指令

RDRAND比RDSEED稍早,并提供([https://software.intel.com/content/www/us/en/develop/blogs/the-difference-between-rdrand-and-rdseed.html 英特尔声称的)CSPRNG。 它的存在由CPUID leaf,ECX位30表示:

mov eax, 1     ; set EAX to request function 1
mov ecx, 0     ; set ECX to request subfunction 0
cpuid
shr ecx, 30    ; the result we want is in ECX...
and ecx, 1     ; ...test for the flag of interest...

如果ECX设置为1(或ZF未设置),则该指令可用。用法与RDSEED相同:

mov ecx, 100   ; number of retries
.retry:
    rdrand eax
    jc .done      ; carry flag is set on success
    loop .retry
.fail:
    ; no random number available
.done:
    ; random number is in EAX

它由RDSEED读取的同一个熵源自动播种,不能手动播种。 因此,它与RDSEED一样需要注意。

Ciphers

通过植入一个安全Cipher(如ChaCha20和AES)并运行多个循环来构造CSPRNG是相当常见的,在这些循环中,输出与运行的计数器一起被重新加密。 每隔一段时间,就会创建一个新密钥,可能涉及另一个安全的随机源。

编写(和植入后门)一个基于CSPRNG的ChaCha20的可能是一篇关于这个主题的有趣文章,以及它如何以令人惊讶的方式出错。

伪随机数生成器

接下来是各种常规PRNG。 它们在质量、简单性和速度的各个方面都有所不同。 仔细阅读每个解释!

标准的示例

直接取自 C标准文档(第347页)

//以下函数定义了rand和srand的可移植实现。

static unsigned long int next = 1;  // 注:“unsigned long int”假定为32位宽

int rand(void)  // RAND_MAX假设为32767
{
    next = next * 1103515245 + 12345;
    return (unsigned int) (next / 65536) % 32768;
}

void srand(unsigned int seed)
{
    next = seed;
}

这是一个相当标准但平庸的标准 线性同余发生器(Linear Congruential Generator-LCG)。 它返回15个随机位。

它基于这个递推公式

 Xn+1 = (aXn + c) mod m

其中,模数m是LCG能产生的最大随机值数。 对于C标准中的示例,a=1103515245、C=12345和m=232(隐式)。 LCG的质量在很大程度上取决于这些值,很难找到好的值。 维基百科给出了常见参数列表

斐波那契随机数

斐波那契序列的特殊“重拍”可以用来生成随机数。 这是通过有4个“种子”来实现的,这些种子一开始是非常奇怪的值(例如45、80、22、32)。 seed()函数的作用是:在序列的末尾添加一个新的种子,并删除第一个种子(种子被称为A、B、C和D)。 rand()函数只返回种子的总和,并用结果调用seed()。 Glidix就是这种RNG实现的一个很好的例子。

线性反馈移位寄存器

线性反馈移位寄存器(LFSR)是一种简单的方法,可以在给定非零种子的情况下生成很长的随机数序列。 其思想如下:“n”位LFSR有“n”位(0或1)。 最初,寄存器用“n”位种子填充。 对于下一个要生成的值,从寄存器中“点击”某些位,并将它们异或在一起。 将结果二进制值输入最左边的位,将所有位向右移动一个位置。 从最右侧LFSR移出的位是生成的随机位。

LFSR可以产生的比特序列最终会重复,但通过仔细选择异或比特,LFSR的周期可以增加到最多2n-1比特。 这意味着在一个2n-1位的序列之后,将返回相同的序列。 然而,这是所有伪随机数生成器都具有的一个特性:在不改变种子的情况下,它们最终会重复自己。

下面是一个16位LFSR的示例,使用位11、13、14和16异或作为其输入。 该LFSR的周期为65535位,因此在序列自身重复之前,它将生成一个65535位的伪随机序列。 LFSR产生的下一个位是1(位16的值),下一个输入位是0。

            1                                       11      13  14      16
          +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
INPUT --> | 0   1   0   0   0   1   0   0   1   1   1   1   0   0   0   1 | --> OUTPUT
          +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

INPUT = bit 11 XOR bit 13 XOR bit 14 XOR bit 16

更大(和更小)的LFSR也是可能的。 聪明人已经推导出多项式,确保在任何非零输入的情况下,LFSR的周期尽可能大(2n-1)。 这样的多项式被写成x16+x14+x13+x11+1。 对于这个“n”=16的多项式示例,位16、14、13和11必须异或在一起,并作为输入提供,从左开始计数,从1开始。 Polynomials for other values of n can be found here on Wikipedia and on page 5 of this PDF document.

请注意,种子永远不能为零。 这也意味着,不可能所有寄存器的位值都为零,而在2个寄存器的可能组合中,不允许全零状态。 因此,2个n-1状态是可能的最大值。

Wichmann-Hill

1982年,Brian Wichmann和David Hill建议组合三个线性同余生成器,然后对结果进行归一化和求和,得到一个均匀分布在0和1之间的数。 一个常见的例子是:

static uint16_t seed[3]; /* seed with numbers between 1 and 30000 */
float wichmann_hill(void) {
  seed[0] = (171 * seed[0]) % 30269;
  seed[1] = (172 * seed[1]) % 30307;
  seed[2] = (170 * seed[2]) % 30323;
  return fmod(seed[0] / 30269.0 + seed[1] / 30307.0 + seed[2] / 30323.0, 1.0);
}

已知该版本的周期略小于7万亿(最常见的倍数为30268、30306和30322)。

Mersenne Twister

Mersenne Twister是一款非常受欢迎的PRNG,可在大多数平台上使用;例如,它是C++11标准库所需的生成器集的一部分。 64位MT-19937版本的C实现如下:

根据维基百科实现:

#include <stddef.h>
#include <stdint.h>
#include <assert.h>

#ifdef USE32
typedef uint32_t word_t;
#define STATE_SIZE  624
#define MIDDLE      397
#define INIT_SHIFT  30
#define INIT_FACT   1812433253
#define TWIST_MASK  0x9908b0df
#define SHIFT1      11
#define MASK1       0xffffffff
#define SHIFT2      7
#define MASK2       0x9d2c5680
#define SHIFT3      15
#define MASK3       0xefc60000
#define SHIFT4      18
#else
typedef uint64_t word_t;
#define STATE_SIZE  312
#define MIDDLE      156
#define INIT_SHIFT  62
#define TWIST_MASK  0xb5026f5aa96619e9
#define INIT_FACT   6364136223846793005
#define SHIFT1      29
#define MASK1       0x5555555555555555
#define SHIFT2      17
#define MASK2       0x71d67fffeda60000
#define SHIFT3      37
#define MASK3       0xfff7eee000000000
#define SHIFT4      43
#endif

#define LOWER_MASK  0x7fffffff
#define UPPER_MASK  (~(word_t)LOWER_MASK)
static word_t state[STATE_SIZE];
static size_t index = STATE_SIZE + 1;

static void seed(word_t s)
{
    index = STATE_SIZE;
    state[0] = s;
    for (size_t i = 1; i < STATE_SIZE; i++)
        state[i] = (INIT_FACT * (state[i - 1] ^ (state[i - 1] >> INIT_SHIFT))) + i;
}

static void twist(void)
{
    for (size_t i = 0; i < STATE_SIZE; i++)
    {
        word_t x = (state[i] & UPPER_MASK) | (state[(i + 1) % STATE_SIZE] & LOWER_MASK);
        x = (x >> 1) ^ (x & 1? TWIST_MASK : 0);
        state[i] = state[(i + MIDDLE) % STATE_SIZE] ^ x;
    }
    index = 0;
}

static word_t mt_random(void)
{
    if (index >= STATE_SIZE)
    {
        assert(index == STATE_SIZE || !"Generator never seeded");
        twist();
    }

    word_t y = state[index];
    y ^= (y >> SHIFT1) & MASK1;
    y ^= (y << SHIFT2) & MASK2;
    y ^= (y << SHIFT3) & MASK3;
    y ^= y >> SHIFT4;

    index++;
    return y;
}

有些实现会自动使用seed 5489为生成器设定种子,但这(显然)会导致每次初始化时都有相同的输出。


它的表现优于上面列出的所有PRNG,但由于其庞大的状态规模,速度相当缓慢。 它在一些统计领域也存在问题。 参考 "It is high time we let go of the Mersenne Twister"

PCG系列

PCG系列是PRNG领域的一个相对较新的成员。 最简单的版本使用带有置换运算符的LCG对输出进行置乱。 它是为了通过尽可能多的统计测试而建造的,同时仍然保持小而快。 There is a comprehensive paper describing the generators and Apache 2.0-licensed code is available: https://www.pcg-random.org/download.html.

请注意,该网站声称,PCG的输出比其他PRNG的输出更难预测,这意味着PCG更安全。 It is possible to predict some generators after only three outputs, so it should not be considered "hard to break" and definitely not "more secure". “非”加密安全PRNG的可预测性通常不是问题。

xoshiro系列

和PCG一样,xoshiro和相关的xoroshiro家族也是相当新的PRNG。

搞一个自创的

如果你仍然不满意,你可能想建立自己的PRNG。 你可以通过编写一些看起来不错的东西,并将其置于自动化的统计测试套件(如TestU01的SmallCrush中,依次Crush,BigCrush。 这些测试非常容易运行,可以快速指出问题,例如:

#include <unif01.h>
#include <bbattery.h>

#include <stdint.h>

static uint32_t next = 1;
unsigned int custom_rand(void) {
    // This is a terrible generator!
    next = (next * 0x4B4B9656U) ^ (next * 0x565AC3C3U) + 1;
    return next * (next - 1);
}

int main(void) {
    unif01_Gen *gen = unif01_CreateExternGenBits("custom_rand", custom_rand);
    bbattery_SmallCrush(gen);  //或者bbattery_Crush,bbattery_BigCrush
    unif01_DeleteExternGenBits(gen);
}

SmallCrush将报告该生成器在15次统计测试中有12次失败。 因此,也不需要进行其他速度慢得多的测试。