如何解决 Python 分叉进程中 OpenSSL 的 PRNG 限制?

How to work around OpenSSL's PRNG limitation in Python forked processes?

Python 2.7.9 标准库中 "ssl" 模块的文档说:

If using this module as part of a multi-processed application (using, for example the multiprocessing or concurrent.futures modules), be aware that OpenSSL’s internal random number generator does not properly handle forked processes. Applications must change the PRNG state of the parent process if they use any SSL feature with os.fork(). Any successful call of RAND_add(), RAND_bytes() or RAND_pseudo_bytes() is sufficient.

具体是什么意思?我应该在每次 fork 之后调用主进程中的这三个函数之一吗?

此外,这是否适用于多线程?

如果相关,我在 Gunicorn 服务器上有一个 Django 应用程序 运行。主进程不对 SSL 做任何事情,但工作进程做。

What does it really mean in concrete terms?

我相信细节在 #18747 中。

主要风险是每个 child 都会得到 PRNG 状态的副本。如果你分叉两个 children 而不触及中间的 PRNG,它们可能会得到相同的 PRNG 状态,大概允许预测攻击。 NIST 的 CVE 2013-1900 将针对 Postgres 的等效攻击向量描述为 "generates insufficiently random numbers, which might allow remote authenticated users to have an unspecified impact"。不是那么具体……

The master process doesn't do anything with SSL, but the workers do.

好吧,问题是可能在两个分叉之间不对 SSL 做任何事情,所以从不对 SSL 做任何事情几乎可以保证……

除了如果你完全确定你甚至从未初始化 SSL,那么 children 将在他们第一次为自己初始化(和播种)SSL需要它。我不确定有什么好方法可以说服自己这种情况正在发生,但可能值得一试。 (也许在启动时 monkeypatch SSL,然后在 fork 后取消修补,所以如果你间接接触它,一切都会死掉?)

Should I call one of those three functions in the master process after each fork?

我宁可谨慎行事。这会很难做到吗,还是效率问题?

如果这很困难,您可以始终在每个 child 的开头播种 RNG。* 对于传统的预分叉服务器,为每个连接启动一个 worker,这可能会非常低效,但我很确定 gunicorn doesn't work that way。它在启动时创建了一个工作池(我认为默认为 2*NCPU+1),也许它偶尔会回收它们,但是新客户要么等待一个空闲的工作人员(在同步模型中),要么进入一个工作人员的 gevent/asyncio/thread/etc.池(在各种异步模型中)。所以 child 的启动时间成本应该是无关紧要的。 (如果我错了,请忽略这个猜测——或者,更好的是,纠正我的无知……)

Also, does this apply to multiple threads?

不,线程将(可变地)共享相同的 RNG 状态,而不是获取它的副本,所以你在那里是安全的。 (当然,gunicorn/gevent 微线程也是如此。)


* 我最初建议或取消初始化 OpenSSL 并重新初始化它,结果是如果你这样做,至少如果主服务器或服务器中有线程,一切都会变得糟糕。所以不要那样做,只是种子。