4. PQC随机数源替换与熵源质量验证

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指针即可,逻辑不变。

替换后必须确认库内部没有绕过该接口直接调用系统RNG。可用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×bitlen0.4998×bitlen偏差<1%
公钥字节熵7.998 bit/byte7.997 bit/byte≥7.99
重复采样数00=0
频率检验通过率1000/10001000/1000≥990/1000
游程检验通过率998/1000999/1000≥990/1000
表中数值为教学示例,实际结果取决于具体熵源与采样量。若替换后字节熵低于7.99或汉明重量偏差超过1%,应检查硬件RNG驱动是否返回了未初始化数据,或系统熵池是否在容器中受限。
PQC随机数源替换与熵源质量验证链路 系统熵源 getrandom() /dev/random 硬件RNG /dev/hwrng TPM / SE / TRNG randombytes() 替换 统一回调注入 RNG_BACKEND 切换 链接覆盖默认实现 PQC库 Kyber / Dilithium 密钥生成 / 封装 / 签名 验证层 重复采样 / 频率 / 游程 汉明重量 / 字节熵 退化则回退检查 判定条件 duplicate = 0 无重复采样 freq_pass ≥ 990 0/1分布均衡 runs_pass ≥ 990 游程数正常 熵 ≥ 7.99 字节熵达标 四项全部通过 → 密钥生成无退化;任一项不通过 → 检查熵源驱动与采样量

常见退化模式与排查

  • 重复采样非零:硬件RNG驱动未正确初始化,或系统熵池在容器中未就绪。检查/dev/hwrng是否存在、getrandom()是否返回EAGAIN。
  • 频率检验通过率低:熵源输出存在偏置,常见于未校准的TRNG。需在驱动层做后处理(如Von Neumann校正)或更换熵源。
  • 字节熵偏低:采样长度过短或熵源被缓存。增加采样量至1000组以上,并确认每次调用都实际读取了熵源。
  • 密钥生成失败:替换接口返回值未检查,库内部将失败当作成功继续执行。确保randombytes返回非零时库能正确中止。
验证通过后,建议将rng_verify纳入CI流水线,每次更换熵源或升级PQC库时自动运行,防止随机数源被意外回退。