强制 OpenSSL 的 RNG 生成可重复的字节序列

6
对于加密工具的单元测试,我希望强制 OpenSSL 的随机数生成器(包括 RAND_bytesRAND_pseudo_bytes)返回可预测、可重复的字节序列,以便各种密文可以依次可预测并被嵌入到测试向量中。 (所有其他密钥材料均在我的控制之下。)

我知道这完全破坏了安全性。这只会用于单元测试。

我不能在每个测试之前简单地使用固定种子调用 RAND_seed,因为(似乎)RNG 会自动从 /dev/urandom 中种子化,无论我是否想要种子化,并且无论如何 RAND_seed 都不会 重置 RNG,它只会将种子添加到熵池中。
有没有办法实现这一点? (在极端情况下,看起来我可以编写自己的 PRNG 引擎,但我认为还应该有更简单的选项。)

1
你不需要一个伪随机数生成器。你只需要一个可预测的数值流;即使是一个计数器也可以。 - Michael Foukarakis
2个回答

8
您可以在运行时将FIPS ANSI X9.31 RNG强制进入测试模式,但不能对SSLeay RNG(默认值)进行操作。如果使用-DPREDICT重新编译OpenSSL,则默认RNG会输出可预测的数字序列,但这不是很方便。 RAND_pseudo_bytes函数生成可预测的数字序列,这意味着它不会像RAND_bytes那样自动添加熵。但正如您注意到的,只能向种子添加熵,而无法显式提供种子,因此在程序运行之间,您将获得不同的数字。这也没有什么帮助。
但编写自己的可预测RNG引擎并不困难。实际上,我会通过使用stdlib的rand()来创建一个rand引擎:
#include <cstdio>
#include <cstdlib>
#include <cassert>
#include <openssl/rand.h>

// These don't need to do anything if you don't have anything for them to do.
static void stdlib_rand_cleanup() {}
static void stdlib_rand_add(const void *buf, int num, double add_entropy) {}
static int stdlib_rand_status() { return 1; }

// Seed the RNG.  srand() takes an unsigned int, so we just use the first
// sizeof(unsigned int) bytes in the buffer to seed the RNG.
static void stdlib_rand_seed(const void *buf, int num)
{
        assert(num >= sizeof(unsigned int));
        srand( *((unsigned int *) buf) );
}

// Fill the buffer with random bytes.  For each byte in the buffer, we generate
// a random number and clamp it to the range of a byte, 0-255.
static int stdlib_rand_bytes(unsigned char *buf, int num)
{
        for( int index = 0; index < num; ++index )
        {
                buf[index] = rand() % 256;
        }
        return 1;
}

// Create the table that will link OpenSSL's rand API to our functions.
RAND_METHOD stdlib_rand_meth = {
        stdlib_rand_seed,
        stdlib_rand_bytes,
        stdlib_rand_cleanup,
        stdlib_rand_add,
        stdlib_rand_bytes,
        stdlib_rand_status
};

// This is a public-scope accessor method for our table.
RAND_METHOD *RAND_stdlib() { return &stdlib_rand_meth; }

int main()
{
        // If we're in test mode, tell OpenSSL to use our special RNG.  If we
        // don't call this function, OpenSSL uses the SSLeay RNG.
        int test_mode = 1;
        if( test_mode )
        {
                RAND_set_rand_method(RAND_stdlib());
        }

        unsigned int seed = 0x00beef00;
        unsigned int rnum[5];

        RAND_seed(&seed, sizeof(seed));
        RAND_bytes((unsigned char *)&rnum[0], sizeof(rnum));
        printf("%u %u %u %u %u\n", rnum[0], rnum[1], rnum[2], rnum[3], rnum[4]);

        return 0;
}

每次运行这个程序,它都使用相同的数字来初始化srand(),因此每次都会得到相同的随机数序列。
corruptor:scratch indiv$ g++ rand.cpp -o r -lcrypto -g
corruptor:scratch indiv$ ./r
1547399009 981369121 2368920148 925292993 788088604
corruptor:scratch indiv$ ./r
1547399009 981369121 2368920148 925292993 788088604
corruptor:scratch indiv$ 

谢谢!OpenSSL的文档给了我一个印象,覆盖内部随机数生成器比我想象的要困难得多,这就是为什么我一直不愿意去做的原因。 - zwol
文档还强烈暗示应该使用ENGINE而不是RAND_METHOD来完成这个任务,但我无法弄清楚如何实现自己的ENGINE。你能对此发表评论吗? - zwol
@Zack:ENGINE 是一个容器,它公开了一个接口,用于动态加载和释放算法实现。就是这样。因此,如果您要将 RNG 引擎实现为 ENGINE 对象,则首先要实现我上面展示的内容,然后您将在其周围实现一个 ENGINE 包装器,以获得 ENGINE 接口的所有好处。算法接口保持不变(RAND_* 函数和 RAND_METHOD 表)。如果您实现了 ENGINE,则仍应通过提供表访问器来支持使用 -DOPENSSL_NO_ENGINE 编译的 OpenSSL 用户。 - indiv

1

为该库编写一个包装器。然后在测试时将其替换为返回您神奇值的模拟器。

记住,在单元测试中,您不是在尝试测试OpenSSL。您正在尝试测试您的代码。


这将会像为 OpenSSL 编写自己的 PRNG 引擎一样困难。让 OpenSSL 的 PRNG 做我想要的事情“应该”更容易些。 - zwol
好的,那就欺骗测试项目的链接器。在源模块中实现自己的RAND_bytes()和RAND_pseudo_bytes(),它将在任何库被引入之前被链接。 - John Deters
“实现自己的[密码学伪随机数生成器]”就像编写自己的引擎一样困难。问题的关键在于,如果可以避免这样做,我不想这样做。 - zwol
1
抱歉,我的表述不够清晰。你自己的RAND_bytes()函数只是一个简单的模拟实现,用于返回静态测试数据,而不是真正的随机引擎。它会像这样:RAND_bytes(unsigned char* buf, int num){memcpy(buf, testArray[offset], num); offset+=num; return 1;}。你需要预先创建testArray中的所有随机数据,并在其中添加一些错误处理。但你要求返回非随机数据而不是随机数据,这是一种方法。 - John Deters

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接