4. PQC随机数源替换与熵源质量验证
为什么PQC库的默认随机数源需要替换
Kyber/Dilithium等PQC算法的密钥生成、封装与签名都依赖随机数源。库自带的默认RNG通常只是rand()或/dev/urandom的简单封装,在嵌入式、容器或高安全场景下存在三类风险:熵不足导致密钥空间被压缩、随机源可预测导致私钥可被恢复、以及不同平台行为不一致导致测试结果不可复现。因此需要把默认RNG替换为指定系统熵源(如getrandom()、/dev/random)或硬件RNG(如TPM、SE、SoC TRNG),并验证替换后密钥生成没有退化。
替换接口与熵源接入点
以常见的PQC库(如liboqs、PQClean)为例,随机数源通常通过一个回调函数或宏注入。核心思路是:库内部所有随机字节请求都汇聚到一个randombytes()函数,我们只需实现该函数并链接到库中,即可完成替换。下面给出一个可编译的替换实现,同时支持系统熵源与硬件RNG两种后端。
/* rng_override.c —— 替换PQC库默认随机数源 */
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/random.h>
#include <errno.h>
/* 后端选择:0=系统getrandom,1=硬件RNG设备 */
#define RNG_BACKEND 0
#define HW_RNG_DEV "/dev/hwrng"
/* 库要求的随机字节接口,签名必须与库声明一致 */
int randombytes(uint8_t *out, size_t outlen)
{
size_t off = 0;
#if RNG_BACKEND == 0
while (off < outlen) {
ssize_t n = getrandom(out + off, outlen - off, 0);
if (n < 0) {
if (errno == EINTR) continue;
return -1;
}
off += (size_t)n;
}
#else
int fd = open(HW_RNG_DEV, O_RDONLY);
if (fd < 0) return -1;
while (off < outlen) {
ssize_t n = read(fd, out + off, outlen - off);
if (n <= 0) { close(fd); return -1; }
off += (size_t)n;
}
close(fd);
#endif
return 0;
}
编译时用-DRNG_BACKEND=1即可切换到硬件RNG。若库使用OQS_randombytes_custom_algorithm()注册回调,则改为调用该注册函数传入randombytes指针即可,逻辑不变。
ltrace或strace观察密钥生成过程中是否仍出现对/dev/urandom的直接读取。熵源质量验证:重复采样与统计检验
替换完成后,不能只看“能跑通”,必须验证输出没有退化。验证分两层:第一层是重复采样,检查相同输入下密钥是否不同、随机字节是否重复;第二层是统计检验,用NIST SP 800-22的简化子集(频率、块频率、游程、近似熵)判断随机性是否达标。
下面给出一个采样与检验脚本,直接调用替换后的randombytes采集数据并计算四项指标。
/* rng_verify.c —— 重复采样 + 简化统计检验 */
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include <stdint.h>
extern int randombytes(uint8_t *out, size_t outlen);
#define N_SAMPLES 1000
#define SAMPLE_LEN 64
static int freq_test(const uint8_t *b, size_t n) {
size_t ones = 0;
for (size_t i = 0; i < n; i++)
for (int j = 0; j < 8; j++) ones += (b[i] >> j) & 1;
double p = (double)ones / (n * 8);
return (p > 0.49 && p < 0.51) ? 1 : 0;
}
static int runs_test(const uint8_t *b, size_t n) {
size_t runs = 1;
for (size_t i = 1; i < n * 8; i++) {
int cur = (b[i/8] >> (i%8)) & 1;
int prv = (b[(i-1)/8] >> ((i-1)%8)) & 1;
if (cur != prv) runs++;
}
double exp = n * 8 / 2.0;
return (runs > exp * 0.9 && runs < exp * 1.1) ? 1 : 0;
}
int main(void) {
uint8_t buf[SAMPLE_LEN];
uint8_t first[SAMPLE_LEN];
int dup = 0, freq_ok = 0, runs_ok = 0;
for (int s = 0; s < N_SAMPLES; s++) {
if (randombytes(buf, SAMPLE_LEN) != 0) {
fprintf(stderr, "randombytes failed at sample %d\n", s);
return 1;
}
if (s == 0) memcpy(first, buf, SAMPLE_LEN);
else if (memcmp(first, buf, SAMPLE_LEN) == 0) dup++;
freq_ok += freq_test(buf, SAMPLE_LEN);
runs_ok += runs_test(buf, SAMPLE_LEN);
}
printf("samples=%d duplicate=%d freq_pass=%d runs_pass=%d\n",
N_SAMPLES, dup, freq_ok, runs_ok);
return 0;
}
编译运行:gcc rng_override.c rng_verify.c -o rng_verify && ./rng_verify。预期输出中duplicate=0,freq_pass与runs_pass接近1000。若duplicate>0说明采样重复,熵源可能被缓存或固定;若freq_pass明显低于1000,说明0/1分布偏斜,熵源质量不足。
密钥生成无退化验证
随机数源替换的最终目的是保证密钥生成质量。需要对比替换前后生成的密钥在统计特征上是否一致。具体做法:用替换后的RNG生成1000组Kyber768密钥对,提取公钥字节,计算其汉明重量分布与字节熵,并与默认RNG的结果对比。若替换后公钥的汉明重量均值仍在0.5×bitlen附近、字节熵接近8.0,则判定无退化。
| 检验项 | 默认RNG(示例) | 替换后(示例) | 通过条件 |
|---|---|---|---|
| 公钥汉明重量均值 | 0.5001×bitlen | 0.4998×bitlen | 偏差<1% |
| 公钥字节熵 | 7.998 bit/byte | 7.997 bit/byte | ≥7.99 |
| 重复采样数 | 0 | 0 | =0 |
| 频率检验通过率 | 1000/1000 | 1000/1000 | ≥990/1000 |
| 游程检验通过率 | 998/1000 | 999/1000 | ≥990/1000 |
常见退化模式与排查
- 重复采样非零:硬件RNG驱动未正确初始化,或系统熵池在容器中未就绪。检查
/dev/hwrng是否存在、getrandom()是否返回EAGAIN。 - 频率检验通过率低:熵源输出存在偏置,常见于未校准的TRNG。需在驱动层做后处理(如Von Neumann校正)或更换熵源。
- 字节熵偏低:采样长度过短或熵源被缓存。增加采样量至1000组以上,并确认每次调用都实际读取了熵源。
- 密钥生成失败:替换接口返回值未检查,库内部将失败当作成功继续执行。确保
randombytes返回非零时库能正确中止。
rng_verify纳入CI流水线,每次更换熵源或升级PQC库时自动运行,防止随机数源被意外回退。