一、问题背景
在做交换设备升级tls1.3,之前做的升级都很顺利,但最近有一台交换设备升级后开机时卡住。然后编译了一版x86测试,x86上开机正常,测试tls1.3也是升级成功的。调试了代码,设备启动时初始LoginEncrypt模块时要生成Rsa密钥对,结果就是卡在这个函数里导致启动不了。设备信息: AMRv7芯片,带有Linux操作系统。
二、阻塞原因(结论)
开头先给结论: 生成密钥前需要获取随机数,在linux系统中随机数生成依赖字符设备/dev/random,当系统运行时各种交互带来的随机熵会存入到random中,生成随机数时就会往该设备里读随机字符,如果熵不够,读取设备时就会发生阻塞。还有一种字符设备/dev/urandom不会发生阻塞,熵不够时只是生成的随机数质量比较低,所以阻塞是因为os的/dev目录下没有urandom,只有random,又是在开机时需要随机数,导致熵不够发生阻塞。
三、调试记录
首先开源代码里是没有RSACreateKey函数的,这一般是我们使用时自己写的函数,源码提供了RSA_generate_key函数,所以就从这个函数开始往下追。
RSA *RSA_generate_key(int bits, unsigned long e_value,
void (*callback) (int, int, void *), void *cb_arg)
{
/***/
if (RSA_generate_key_ex(rsa, bits, e, cb)) {
BN_free(e);
BN_GENCB_free(cb);
return rsa;
}
/***/
}
我们根据RSA_generate_key的源码,继续往下查RSA_generate_key_ex函数。在继续往下追之前,先考虑一下哪些情况会发生阻塞?我首先怀疑发生阻塞是因为在某个死循环里退不出来,其次是锁。所以后面追代码的时候都优先检查循环和锁。
密钥生成大致步骤就是先生成随机数,然后计算密钥,再检查密钥是否安全,这是个循环操作。其中有不少中间函数都是有for(;;)死循环和goto语句循环的,但非常可惜,这些循环都只执行了一到两次,也遇到过锁,但是实际上锁也没有发挥作用。
最后往下追代码,大致知道了问题出在随机数生成的时候,因为没有生成随机数,导致后面的操作都无法进行。openssl源码中生成随机数的函数是bnrand(),查找openssl的官方文档看看这个函数的信息,很可惜,没有我想看到的bnrand()会发生阻塞的描述。所以只能进函数继续分析。再往下分析,在执行drbg->get_entropy时卡住,因为是个函数指针,实际调用函数如下:
size_t rand_drbg_get_entropy(RAND_DRBG *drbg,
unsigned char **pout,
int entropy, size_t min_len, size_t max_len,
int prediction_resistance)
{
/***/
if (drbg->parent != NULL) {
size_t bytes_needed = rand_pool_bytes_needed(pool, 1 /*entropy_factor*/);
unsigned char *buffer = rand_pool_add_begin(pool, bytes_needed);
if (buffer != NULL) {
size_t bytes = 0;
printf("1");
rand_drbg_lock(drbg->parent);
printf("2");
if (RAND_DRBG_generate(drbg->parent,
buffer, bytes_needed,
prediction_resistance,
(unsigned char *)&drbg, sizeof(drbg)) != 0)
bytes = bytes_needed;
printf("3");
rand_drbg_unlock(drbg->parent);
rand_pool_add_end(pool, bytes, 8 * bytes);
entropy_available = rand_pool_entropy_available(pool);
}
} else {
printf("4");
entropy_available = rand_pool_acquire_entropy(pool);
printf("5");
}
/***/
}
比较奇怪的是,打印的结果是1、2、4,所以猜测RAND_DRBG_generate函数又回调了drbg->get_entropy,然后第二次走的4,就简单追了下RAND_DRBG_generate,确实如此,所以再往下追rand_pool_acquire_entropy好了。
追到这问题快解决了。查看了rand_pool_acquire_entropy函数的定义:
size_t rand_pool_acquire_entropy(RAND_POOL *pool)
{
/***/
# if defined(OPENSSL_RAND_SEED_DEVRANDOM)
if (wait_random_seeded()) {
size_t bytes_needed;
unsigned char *buffer;
size_t i;
bytes_needed = rand_pool_bytes_needed(pool, 1 /*entropy_factor*/);
for (i = 0; bytes_needed > 0 && i < OSSL_NELEM(random_device_paths);
i++) {
ssize_t bytes = 0;
/* Maximum number of consecutive unsuccessful attempts */
int attempts = 3;
const int fd = get_random_device(i);
if (fd == -1)
continue;
while (bytes_needed != 0 && attempts-- > 0) {
buffer = rand_pool_add_begin(pool, bytes_needed);
bytes = read(fd, buffer, bytes_needed);
if (bytes > 0) {
rand_pool_add_end(pool, bytes, 8 * bytes);
bytes_needed -= bytes;
attempts = 3; /* reset counter on successful attempt */
} else if (bytes < 0 && errno != EINTR) {
break;
}
}
if (bytes < 0 || !keep_random_devices_open)
close_random_device(i);
bytes_needed = rand_pool_bytes_needed(pool, 1);
}
entropy_available = rand_pool_entropy_available(pool);
if (entropy_available > 0)
return entropy_available;
}
# endif
/***/
}
最后定位到read函数阻塞,我记得read函数不会阻塞来着,然后查看了一下fd是打开的哪一个文件,文件地址定义在random_device_paths里:
# define DEVRANDOM "/dev/urandom", "/dev/random", "/dev/hwrng", "/dev/srandom"
可以看到优先打开urandom,其次是random。实际debug for循环确实执行了两次,第一次fd -1走了continue进入下一次循环,第二次fd 8,然后read阻塞。所以查了一下random和urandom的资料,确实random刚启动时熵不够会导致阻塞。我又从代码的os目录下查看了/dev的文件情况,没有urandom。然后看看之前其他升级过的设备,os下都有urandom。那么问题很显然就是因为这个了!最后在os目录下找到生成设备文件的脚本,把urandom添加上去,重新编译,终于成功启动了!!
生成urandom字符设备的命令:
$sudo mknod urandom c 1 9