奥门新浦京官方网站PHP中的随机性——你觉得自己幸运吗?

本文由码农网 –
邱康原创翻译,转载请看清文末的转载要求,欢迎参与我们的付费投稿计划!

很多库例程产生的“随机”数是准备用于仿真、游戏等等;它们在被用于密钥生成一类的安全函数时是不够随机的。其问题在于这些库例程使用的算法的未来值可以被攻击者轻易地推导出来(虽然看起来它们可能是随机的)。对于安全函数,需要的随机值应该是基于量子效应之类的确实无法预测的值。Linux内核(1.3.30以上)包括了一个随机数发生器/dev/random,对于很多安全目的是足够的。

bug(端口监听启动后,Tomcat耗时2min+ ):

本文分析了生成用于加密的随机数的相关问题。 PHP 5没有提供一种简单的机制来生成密码学上强壮的随机数,但是PHP 7通过引入几个CSPRNG函数来解决了这个问题。

/dev/random 是如何创建随机数的呢?

2017-09-01 15:51:05.146  WARN 20923 --- [http-nio-80-exec-1] o.a.c.util.SessionIdGeneratorBase        : Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [141,273] milliseconds.
2017-09-01 15:51:05.160  WARN 20923 --- [http-nio-80-exec-4] o.a.c.util.SessionIdGeneratorBase        : Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [67,573] milliseconds.
2017-09-01 15:51:05.160  WARN 20923 --- [http-nio-80-exec-3] o.a.c.util.SessionIdGeneratorBase        : Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [72,205] milliseconds.
2017-09-01 15:51:05.163  WARN 20923 --- [http-nio-80-exec-2] o.a.c.util.SessionIdGeneratorBase        : Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [132,321] milliseconds.
2017-09-01 18:24:26.447  WARN 21241 --- [http-nio-80-exec-1] o.a.c.util.SessionIdGeneratorBase        : Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [166,806] milliseconds.
2017-09-01 20:11:12.953  WARN 21508 --- [http-nio-80-exec-1] o.a.c.util.SessionIdGeneratorBase        : Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [173,287] milliseconds.
2017-09-03 16:50:59.385  WARN 25289 --- [http-nio-80-exec-1] o.a.c.util.SessionIdGeneratorBase        : Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [154,563] milliseconds.

奥门新浦京官方网站 1

Linux
操作系统提供本质上随机(或者至少具有强烈随机性的部件)的库数据。这些数据通常来自于设备驱动程序。例如,键盘驱动程序收集两个按键之间时间的信息,然后将这个环境噪声填入随机数发生器库。

这个SecureRandom的初始化竟然花了100秒之多。。。

什么是CSPRNG

引用维基百科,一个密码学上安全的伪随机数发生器(Cryptographically Secure Pseudorandom Number Generator 缩写CSPRNG)是一个伪随机数生成器(PRNG),其生成的伪随机数适用于密码学算法。

CSPRNG可能主要用于:

  • 密钥生成(例如,生成复杂的密钥)
  • 为新用户产生随机的密码
  • 加密系统

获得高级别安全性的一个关键方面就是高品质的随机性

随机数据存储在
熵池中,它在每次有新数据进入时进行“搅拌”。这种搅拌实际上是一种数学转换,帮助提高随机性。当数据添加到熵池中后,系统估计获得了多少真正随机位。

后来查了一下,发现这个问题抱怨的还是蛮多的,以至于tomcat的wiki里面还单独列出来作为加速启动的一个方面:

PHP7 中的CSPRNG

PHP 7引入了两个新函数可以用来实现CSPRNG: random_bytes 和 random_int。

random_bytes 函数返回一个字符串,接受一个int型入参代表返回结果的字节数。

例子:

$bytes = random_bytes('10');
var_dump(bin2hex($bytes));
//possible ouput: string(20) "7dfab0af960d359388e6"

random_int 函数返回一个指定范围内的int型数字。

例子:

var_dump(random_int(1, 100));
//possible output: 27

测定随机性的总量是很重要的。问题是某些量往往比起先考虑时看上去的随机性小。例如,添加表示自从上次按键盘以来秒数的
32 位数实际上并没有提供新的 32 位随机信息,因为大多数按键都是很接近的。

Tomcat 7+ heavily relies on SecureRandom class to provide random values
for its session ids and in other places. Depending on your JRE it can
cause delays during startup if entropy source that is used to initialize
SecureRandom is short of entropy. You will see warning in the logs when
this happens.

后台运行环境

以上函数的随机性不同的取决于环境:

  • 在window上,CryptGenRandom()总是被使用。
  • 在其他平台,arc4random_buf()如果可用会被使用(在BSD系列或者具有libbsd的系统上成立)
  • 以上都不成立的话,一个linux系统调用getrandom(2)会被使用。
  • 如果还不行,/dev/urandom 会被作为最后一个可使用的工具
  • 如果以上都不行,系统会抛出错误

从 /dev/random 中读取字节后,熵池就使用 MD5
算法进行密码散列,该散列中的各个字节被转换成数字,然后返回。

 

一个简单的测试

一个好的随机数生成系统保证合适的产生“质量”。为了检查这个质量, 通常要执行一连串的统计测试。不需要深入研究复杂的统计主题,比较一个已知的行为和数字生成器的结果可以帮助质量评价。

一个简单的测试是骰子游戏。假设掷1个骰子1次得到结果为6的概率是1/6,那么如果我同时掷3个骰子100次,得到的结果粗略如下:

  • 0 个6 = 57.9 次
  • 1 个6 = 34.7次
  • 2 个6 = 6.9次
  • 3 个6 = 0.5次

以下是是实现实现掷骰子1,000,000次的代码:

$times = 1000000;
$result = [];
for ($i=0; $i<$times; $i++){
    $dieRoll = array(6 => 0); //initializes just the six counting to zero
    $dieRoll[roll()] += 1; //first die
    $dieRoll[roll()] += 1; //second die
    $dieRoll[roll()] += 1; //third die
    $result[$dieRoll[6]] += 1; //counts the sixes
}
function roll(){
    return random_int(1,6);
}
var_dump($result);

用PHP7 的 random_int 和简单的 rand 函数可能得到如下结果

Sixes expected random_int
0 579000 579430
1 347000 346927
2 69000 68985
3 5000 4658

如果先看到rand 和 random_int 更好的比较我们可以应用一个公式把结果画在图上。公式是:(php结果-期待的结果)/期待结果的0.5次方。

结果图如下:

奥门新浦京官方网站 2

(接近0的值更好)

尽管3个6的结果表现不好,并且这个测试对实际应用来说太过简单我们仍可以看出 random_int 表现优于 rand.

进一步,我们的应用的安全级别由于不可预测性和随机数发生器的可重复行为而得到提升。

如果在熵池中没有可用的随机性位, /dev/random
在池中有足够的随机性之前等待,不返回结果。这意味着如果使用 /dev/random
来产生许多随机数,就会发现它太慢了,不够实用。我们经常看到 /dev/random
生成几十字节的数据,然后在许多秒内都不产生结果。

There is a way to configure JRE to use a non-blocking entropy source by
setting the following system
property: -Djava.security.egd=file:/dev/./urandom

PHP5 呢

缺省情况下,PHP5 不提供强壮的随机数发生器。实际上,还是有选择的比如 openssl_random_pseudo_bytes(), mcrypt_create_iv() 或者直接使用fread()函数来使用 /dev/random 或 /dev/urandom 设备。也有一些包比如 RandomLib 或 libsodium.

如果你想要开始使用一个更好的随机数发生器并且同时准备好使用PHP7,你可以使用Paragon Initiative Enterprises random_compat 库。 random_compat 库允许你在 PHP 5.x project.使用 random_bytes() and random_int()

这个库可以通过Composer安装:

composer require paragonie/random_compat

require 'vendor/autoload.php';
$string = random_bytes(32);
var_dump(bin2hex($string));
// string(64) "8757a27ce421b3b9363b7825104f8bc8cf27c4c3036573e5f0d4a91ad2aaec6f"
$int = random_int(0,255);
var_dump($int);
// int(81)

random_compat 库和PHP7使用不同的顺序:

fread() /dev/urandom if available
mcrypt_create_iv($bytes, MCRYPT_CREATE_IV)
COM('CAPICOM.Utilities.1')->GetRandom()
openssl_random_pseudo_bytes()

想知道为什么是这个顺序建议阅读 documentation.

这个库的一个简单应用用来产生密码:

$passwordChar = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$passwordLength = 8;
$max = strlen($passwordChar) - 1;
$password = '';
for ($i = 0; $i < $passwordLength; ++$i) {
    $password .= $passwordChar[random_int(0, $max)];
}
echo $password;
//possible output: 7rgG8GHu

幸运的是有熵池的另一个接口可以绕过这个限制:/dev/urandom。即使熵池中没有随机性可用,这个替代设备也总是返回随机数。如果您取出许多数而不给熵池足够的时间重新充满,就再也不能获得各种来源的合用熵的好处了;但您仍可以从熵池的
MD5 散列中获得非常好的随机数!这种方式的问题是,如果有任何人破解了 MD5
算法,并通过查看输出了解到有关散列输入的信息,那么您的数就会立刻变得完全可预料。大多数专家都认为这种分析从计算角度来讲是不可行的。然而,仍然认为
/dev/urandom 比 /dev/random 要“不安全一些”(并通常值得怀疑)。

尝试使用-Djava.security.egd=file:/dev/./urandom启动了一下,果然快了很多。

总结

你总是应该使用一个密码学上安全的伪随机数生成器,random_compat 库提供了一种好的实现。

如果你想要使用可靠的随机数据源,如你在本文所见,建议尽快使用 random_int 和 random_bytes.

应用中出现的问题:

不过tomcat的wiki中提到,如果使用这个非阻塞的/dev/urandom的话,会有一些安全方面的风险,这块我倒确实不太明白,不过好在有明白人,而且还写了一篇长文来证明使用/dev/urandom是没问题的,所以就先用着吧:-)

在我们的服务器程序中,用户登陆的时候会读取/dev/random产生随机数,问题来了,当用户登陆比较密集,这时候read就会返回特别慢,并且返回的字节数也比要求的少,甚至不返回――阻塞。我们把用户登陆处理函数放在了线程池里,导致的问题就是线程池里所有线程都可能会阻塞,这就造成了拒绝服务攻击。导致其他用户登陆失败。

 

CODE:

另外:

 1 #include <stdio.h>
 2 #include <string.h>
 3 #include <sys/types.h>
 4 #include <sys/stat.h>
 5 #include <sys/file.h>
 6 #include <sys/time.h>
 7 #include <errno.h>
 8 #include <unistd.h>
 9 #include <stdlib.h>
 10

 11 static int get_random_fd (void)
 12 {
 13     static int fd = -2;
 14
 15     if (fd == -2)
 16     {
 17         fd = open ("/dev/random", O_RDONLY | O_NONBLOCK);
 18         if (fd == -1)
 19         fd = open ("/dev/urandom", O_RDONLY | O_NONBLOCK);
 20     }
 21
 22     return fd;
 23 }
 24
 25 /*
 26 * Generate a series of random bytes. Use /dev/random if possible,
 27 * and if not, use /dev/urandom.
 28 */
 29 void get_random_bytes(void* buf, int nbytes)
 30 {
 31 int i, fd = get_random_fd();
 32 int lose_counter = 0;
 33 char cp = (char)buf;
 34 struct timeval tv;
 35 static unsigned seed = 0;
 36
 37 if (fd >= 0)
 38 {
 39 while (nbytes > 0)
 40 {
 41 i = read (fd, cp, nbytes);
 42 if ((i < 0) &&
 43 ((errno == EINTR) || (errno == EAGAIN)))
 44 continue;
 45
 46 if (i <= 0)
 47 {
 48 if (lose_counter++ == 8)
 49 break;
 50
 51 continue;
 52 }
 53 nbytes -= i;
 54 cp += i;
 55 lose_counter = 0;
 56 }
 57 }
 58
 59 for (i = 0; i < nbytes; i++)
 60 {
 61 if (seed == 0)
 62 {
 63 gettimeofday(&tv, 0);
 64 seed = (getpid() << 16) ^ getuid() ^ tv.tv_sec ^ tv.tv_usec;
 65 }
 66 *cp++ = rand_r(&seed) & 0xFF;
 67 }
 68
 69 return;
 70 }

有两种解决办法:

解决方案:

1.在Tomcat环境中解决

1–3行:  定义fd为静态变量,这样只打开一次设备。
  17 –
19行: 无阻塞模式打开/dev/random设备。如果该设备打开失败尝试打开/dev/urandom。
  29行:  void get_random_bytes(void* buf, int
nbytes)函数是提供给用户的接口,用户调用这个函数就可以得到随机数。
  37-57行: read有可能返回的字节数小于请求的字节数。这时候就循环读直到读够了所请求的大小。这样最多重复8次。然后返回。
  59-67行: 如果上面重复8次都没有读够所请求的字节数,则我们自己生成随机数来填充。
  注意:打开的fd我们并没有关闭,请您根据自己需求在合适的地方关闭。

可以通过配置JRE使用非阻塞的Entropy Source。

在catalina.sh中加入这么一行:-Djava.security.egd=file:/dev/./urandom
即可。

加入后再启动Tomcat,整个启动耗时下降到Server startup in 6213
ms,大大降低了启动的时间。

2.在JVM环境中解决

先执行which javac命令检查jdk安装路径

/usr/local/java/jdk1.8.0_92/bin/javac

去到$JAVA_PATH/jre/lib/security/java.security这个文件,找到下面的内容:

securerandom.source=file:/dev/urandom

替换成

securerandom.source=file:/dev/./urandom

 

这样问题就解决了

 

 

在apache-tomcat官方文档:
如何让tomcat启动更快里面提到了一些启动时的优化项,其中一项是关于随机数生成时,采用的“熵源”(entropy
source)的策略。

他提到tomcat7的session
id的生成主要通过java.security.SecureRandom生成随机数来实现,随机数算法使用的是”SHA1PRNG”

private String secureRandomAlgorithm = "SHA1PRNG";

在sun/oracle的jdk里,这个算法的提供者在底层依赖到操作系统提供的随机数据,在linux上,与之相关的是/dev/random/dev/urandom,对于这两个设备块的描述以前也见过讨论随机数的文章,
wiki中有比较详细的描述,摘抄过来,先看/dev/random
在读取时,/dev/random设备会返回小于熵池噪声总数的随机字节。
/dev/random可生成高随机性的公钥或一次性密码本。
若熵池空了,对/dev/random的读操作将会被阻塞,直到收集到了足够的环境噪声为止

/dev/urandom则是一个非阻塞的发生器:

 

dev/random的一个副本是/dev/urandom
(”unlocked”,非阻塞的随机数发生器),它会重复使用熵池中的数据以产生伪随机数据。
这表示对/dev/urandom的读取操作不会产生阻塞,但其输出的熵可能小于/dev/random的。
它可以作为生成较低强度密码的伪随机数生成器,不建议用于生成高强度长期密码。

另外wiki里也提到了为什么linux内核里的随机数生成器采用SHA1散列算法而非加密算法,是为了避开法律风险(密码出口限制)。

回到tomcat文档里的建议,采用非阻塞的熵源(entropy
source),通过java系统属性来设置:

-Djava.security.egd=file:/dev/./urandom

这个系统属性egd表示熵收集守护进程(entropy gathering
daemon),但这里值为何要在devrandom之间加一个点呢?是因为一个jdk的bug,在这个bug的连接里有人反馈及时对
securerandom.source
设置为/dev/urandom它也仍然使用的/dev/random,有人提供了变通的解决方法,其中一个变通的做法是对securerandom.source设置为/dev/./urandom才行。也有人评论说这个不是bug,是有意为之。

我看了一下我当前所用的jdk7的java.security文件里,配置里仍使用的是/dev/urandom

## Select the source of seed data for SecureRandom. By default an# attempt is made to use the entropy gathering device specified by# the securerandom.source property. If an exception occurs when# accessing the URL then the traditional system/thread activity# algorithm is used.## On Solaris and Linux systems, if file:/dev/urandom is specified and it# exists, a special SecureRandom implementation is activated by default.# This "NativePRNG" reads random bytes directly from /dev/urandom.## On Windows systems, the URLs file:/dev/random and file:/dev/urandom# enables use of the Microsoft CryptoAPI seed functionality.#securerandom.source=file:/dev/urandom

我不确定jdk7里,这个/dev/urandom也同那个bug报告里所说的等同于/dev/random;要使用非阻塞的熵池,这里还是要修改为/dev/./urandom呢,还是jdk7已经修复了这个问题,就是同注释里的意思,只好验证一下。

使用bug报告里给出的代码:

import java.security.SecureRandom;

public class JRand {

      public static void main(String args[]) throws Exception {
System.out.println("Ok: " +SecureRandom.getInstance("SHA1PRNG").nextLong());
     }
}

 

然后设置不同的系统属性来验证,先是在我的mac上:

% time java -Djava.security.egd=file:/dev/urandomJRandOk: 8609191756834777000java -Djava.security.egd=file:/dev/urandom JRand0.11s user 0.03s system 115% cpu 0.117 total

% time java -Djava.security.egd=file:/dev/./urandomJRandOk: -3573266464480299009java -Djava.security.egd=file:/dev/./urandom JRand0.11s user 0.03s system 116% cpu 0.116 total

可以看到/dev/urandom/dev/./urandom的执行时间差不多,有点纳闷,再仔细看一下wiki里说的:

FreeBSD操作系统实现了256位的Yarrow算法变体,以提供伪随机数流。与Linux的/dev/random不同,FreeBSD的/dev/random不会产生阻塞,与Linux的/dev/urandom相似,提供了密码学安全的伪随机数发生器,而不是基于熵池。而FreeBSD的/dev/urandom则只是简单的链接到了/dev/random。

 

尽管在我的mac上/dev/urandom并不是/dev/random的链接,但mac与bsd内核应该是相近的,/dev/random也是非阻塞的,/dev/urandom是用来兼容linux系统的,这两个随机数生成器的行为是一致的。参考这里。

然后再到一台ubuntu系统上测试:

% time java -Djava.security.egd=file:/dev/urandomJRandOk: 6677107889555365492java -Djava.security.egd=file:/dev/urandom JRand0.14s user 0.02s system 9% cpu 1.661 total
% time java -Djava.security.egd=file:/dev/./urandomJRandOk: 5008413661952823775java -Djava.security.egd=file:/dev/./urandom JRand0.12s user 0.02s system 99% cpu 0.145 total

这回差异就完全体现出来了,阻塞模式的熵池耗时用了1.6秒,而非阻塞模式则只用了0.14秒,差了一个数量级,当然代价是转换为对cpu的开销了。

//
补充,连续在ubuntu上测试几次/dev/random方式之后,导致熵池被用空,被阻塞了60秒左右。应用服务器端要避免这种方式。

发表评论

电子邮件地址不会被公开。 必填项已用*标注