Skip to content
This repository was archived by the owner on Jan 27, 2022. It is now read-only.

Commit 19dcf7b

Browse files
committed
Random: Make Crypto.Random.atfork() set last_reseed=None (CVE-2013-1445)
== Summary == In PyCrypto before v2.6.1, the Crypto.Random pseudo-random number generator (PRNG) exhibits a race condition that may cause it to generate the same 'random' output in multiple processes that are forked from each other. Depending on the application, this could reveal sensitive information or cryptographic keys to remote attackers. An application may be affected if, within 100 milliseconds, it performs the following steps (which may be summarized as "read-fork-read-read"): 1. Read from the Crypto.Random PRNG, causing an internal reseed; 2. Fork the process and invoke Crypto.Random.atfork() in the child; 3. Read from the Crypto.Random PRNG again, in at least two different processes (parent and child, or multiple children). Only applications that invoke Crypto.Random.atfork() and perform the above steps are affected by this issue. Other applications are unaffected. Note: Some PyCrypto functions, such as key generation and PKCS#1-related functions, implicitly read from the Crypto.Random PRNG. == Technical details == Crypto.Random uses Fortuna[1] to generate random numbers. The flow of entropy looks something like this: /dev/urandom -\ +-> "accumulator" --> "generator" --> output other sources -/ (entropy pools) (AES-CTR) - The "accumulator" maintains several pools that collect entropy from the environment. - The "generator" is a deterministic PRNG that is reseeded by the accumulator. Reseeding normally occurs during each request for random numbers, but never more than once every 100 ms (the "minimum reseed interval"). When a process is forked, the parent's state is duplicated in the child. In order to continue using the PRNG, the child process must invoke Crypto.Random.atfork(), which collects new entropy from /dev/urandom and adds it to the accumulator. When new PRNG output is subsequently requested, some of the new entropy in the accumulator is used to reseed the generator, causing the output of the child to diverge from its parent. However, in previous versions of PyCrypto, Crypto.Random.atfork() did not explicitly reset the child's rate-limiter, so if the child requested PRNG output before the minimum reseed interval of 100 ms had elapsed, it would generate its output using state inherited from its parent. This created a race condition between the parent process and its forked children that could cause them to produce identical PRNG output for the duration of the 100 ms minimum reseed interval. == Demonstration == Here is some sample code that illustrates the problem: from binascii import hexlify import multiprocessing, pprint, time import Crypto.Random def task_main(arg): a = Crypto.Random.get_random_bytes(8) time.sleep(0.1) b = Crypto.Random.get_random_bytes(8) rdy, ack = arg rdy.set() ack.wait() return "%s,%s" % (hexlify(a).decode(), hexlify(b).decode()) n_procs = 4 manager = multiprocessing.Manager() rdys = [manager.Event() for i in range(n_procs)] acks = [manager.Event() for i in range(n_procs)] Crypto.Random.get_random_bytes(1) pool = multiprocessing.Pool(processes=n_procs, initializer=Crypto.Random.atfork) res_async = pool.map_async(task_main, zip(rdys, acks)) pool.close() [rdy.wait() for rdy in rdys] [ack.set() for ack in acks] res = res_async.get() pprint.pprint(sorted(res)) pool.join() The output should be random, but it looked like this: ['c607803ae01aa8c0,2e4de6457a304b34', 'c607803ae01aa8c0,af80d08942b4c987', 'c607803ae01aa8c0,b0e4c0853de927c4', 'c607803ae01aa8c0,f0362585b3fceba4'] == Solution == The solution is to upgrade to PyCrypto v2.6.1 or later, which properly resets the rate-limiter when Crypto.Random.atfork() is invoked in the child. == References == [1] N. Ferguson and B. Schneier, _Practical Cryptography_, Indianapolis: Wiley, 2003, pp. 155-184.
1 parent 373ea76 commit 19dcf7b

File tree

4 files changed

+196
-0
lines changed

4 files changed

+196
-0
lines changed

Diff for: lib/Crypto/Random/Fortuna/FortunaAccumulator.py

+9
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,15 @@ def __init__(self):
109109
self.pools = [FortunaPool() for i in range(32)] # 32 pools
110110
assert(self.pools[0] is not self.pools[1])
111111

112+
def _forget_last_reseed(self):
113+
# This is not part of the standard Fortuna definition, and using this
114+
# function frequently can weaken Fortuna's ability to resist a state
115+
# compromise extension attack, but we need this in order to properly
116+
# implement Crypto.Random.atfork(). Otherwise, forked child processes
117+
# might continue to use their parent's PRNG state for up to 100ms in
118+
# some cases. (e.g. CVE-2013-1445)
119+
self.last_reseed = None
120+
112121
def random_data(self, bytes):
113122
current_time = time.time()
114123
if (self.last_reseed is not None and self.last_reseed > current_time): # Avoid float comparison to None to make Py3k happy

Diff for: lib/Crypto/Random/_UserFriendlyRNG.py

+15
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,24 @@ def reinit(self):
9090
"""Initialize the random number generator and seed it with entropy from
9191
the operating system.
9292
"""
93+
94+
# Save the pid (helps ensure that Crypto.Random.atfork() gets called)
9395
self._pid = os.getpid()
96+
97+
# Collect entropy from the operating system and feed it to
98+
# FortunaAccumulator
9499
self._ec.reinit()
95100

101+
# Override FortunaAccumulator's 100ms minimum re-seed interval. This
102+
# is necessary to avoid a race condition between this function and
103+
# self.read(), which that can otherwise cause forked child processes to
104+
# produce identical output. (e.g. CVE-2013-1445)
105+
#
106+
# Note that if this function can be called frequently by an attacker,
107+
# (and if the bits from OSRNG are insufficiently random) it will weaken
108+
# Fortuna's ability to resist a state compromise extension attack.
109+
self._fa._forget_last_reseed()
110+
96111
def close(self):
97112
self.closed = True
98113
self._osrng = None

Diff for: lib/Crypto/SelfTest/Random/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def get_tests(config={}):
3232
from Crypto.SelfTest.Random import OSRNG; tests += OSRNG.get_tests(config=config)
3333
from Crypto.SelfTest.Random import test_random; tests += test_random.get_tests(config=config)
3434
from Crypto.SelfTest.Random import test_rpoolcompat; tests += test_rpoolcompat.get_tests(config=config)
35+
from Crypto.SelfTest.Random import test__UserFriendlyRNG; tests += test__UserFriendlyRNG.get_tests(config=config)
3536
return tests
3637

3738
if __name__ == '__main__':

Diff for: lib/Crypto/SelfTest/Random/test__UserFriendlyRNG.py

+171
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# -*- coding: utf-8 -*-
2+
# Self-tests for the user-friendly Crypto.Random interface
3+
#
4+
# Written in 2013 by Dwayne C. Litzenberger <dlitz@dlitz.net>
5+
#
6+
# ===================================================================
7+
# The contents of this file are dedicated to the public domain. To
8+
# the extent that dedication to the public domain is not available,
9+
# everyone is granted a worldwide, perpetual, royalty-free,
10+
# non-exclusive license to exercise all rights associated with the
11+
# contents of this file for any purpose whatsoever.
12+
# No rights are reserved.
13+
#
14+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
18+
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
19+
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
# ===================================================================
23+
24+
"""Self-test suite for generic Crypto.Random stuff """
25+
26+
from __future__ import nested_scopes
27+
28+
__revision__ = "$Id$"
29+
30+
import binascii
31+
import pprint
32+
import unittest
33+
import os
34+
import time
35+
import sys
36+
if sys.version_info[0] == 2 and sys.version_info[1] == 1:
37+
from Crypto.Util.py21compat import *
38+
from Crypto.Util.py3compat import *
39+
40+
try:
41+
import multiprocessing
42+
except ImportError:
43+
multiprocessing = None
44+
45+
import Crypto.Random._UserFriendlyRNG
46+
import Crypto.Random.random
47+
48+
class RNGForkTest(unittest.TestCase):
49+
50+
def _get_reseed_count(self):
51+
"""
52+
Get `FortunaAccumulator.reseed_count`, the global count of the
53+
number of times that the PRNG has been reseeded.
54+
"""
55+
rng_singleton = Crypto.Random._UserFriendlyRNG._get_singleton()
56+
rng_singleton._lock.acquire()
57+
try:
58+
return rng_singleton._fa.reseed_count
59+
finally:
60+
rng_singleton._lock.release()
61+
62+
def runTest(self):
63+
# Regression test for CVE-2013-1445. We had a bug where, under the
64+
# right conditions, two processes might see the same random sequence.
65+
66+
if sys.platform.startswith('win'): # windows can't fork
67+
assert not hasattr(os, 'fork') # ... right?
68+
return
69+
70+
# Wait 150 ms so that we don't trigger the rate-limit prematurely.
71+
time.sleep(0.15)
72+
73+
reseed_count_before = self._get_reseed_count()
74+
75+
# One or both of these calls together should trigger a reseed right here.
76+
Crypto.Random._UserFriendlyRNG._get_singleton().reinit()
77+
Crypto.Random.get_random_bytes(1)
78+
79+
reseed_count_after = self._get_reseed_count()
80+
self.assertNotEqual(reseed_count_before, reseed_count_after) # sanity check: test should reseed parent before forking
81+
82+
rfiles = []
83+
for i in range(10):
84+
rfd, wfd = os.pipe()
85+
if os.fork() == 0:
86+
# child
87+
os.close(rfd)
88+
f = os.fdopen(wfd, "wb")
89+
90+
Crypto.Random.atfork()
91+
92+
data = Crypto.Random.get_random_bytes(16)
93+
94+
f.write(data)
95+
f.close()
96+
os._exit(0)
97+
# parent
98+
os.close(wfd)
99+
rfiles.append(os.fdopen(rfd, "rb"))
100+
101+
results = []
102+
results_dict = {}
103+
for f in rfiles:
104+
data = binascii.hexlify(f.read())
105+
results.append(data)
106+
results_dict[data] = 1
107+
f.close()
108+
109+
if len(results) != len(results_dict.keys()):
110+
raise AssertionError("RNG output duplicated across fork():\n%s" %
111+
(pprint.pformat(results)))
112+
113+
114+
# For RNGMultiprocessingForkTest
115+
def _task_main(q):
116+
a = Crypto.Random.get_random_bytes(16)
117+
time.sleep(0.1) # wait 100 ms
118+
b = Crypto.Random.get_random_bytes(16)
119+
q.put(binascii.b2a_hex(a))
120+
q.put(binascii.b2a_hex(b))
121+
q.put(None) # Wait for acknowledgment
122+
123+
124+
class RNGMultiprocessingForkTest(unittest.TestCase):
125+
126+
def runTest(self):
127+
# Another regression test for CVE-2013-1445. This is basically the
128+
# same as RNGForkTest, but less compatible with old versions of Python,
129+
# and a little easier to read.
130+
131+
n_procs = 5
132+
manager = multiprocessing.Manager()
133+
queues = [manager.Queue(1) for i in range(n_procs)]
134+
135+
# Reseed the pool
136+
time.sleep(0.15)
137+
Crypto.Random._UserFriendlyRNG._get_singleton().reinit()
138+
Crypto.Random.get_random_bytes(1)
139+
140+
# Start the child processes
141+
pool = multiprocessing.Pool(processes=n_procs, initializer=Crypto.Random.atfork)
142+
map_result = pool.map_async(_task_main, queues)
143+
144+
# Get the results, ensuring that no pool processes are reused.
145+
aa = [queues[i].get(30) for i in range(n_procs)]
146+
bb = [queues[i].get(30) for i in range(n_procs)]
147+
res = list(zip(aa, bb))
148+
149+
# Shut down the pool
150+
map_result.get(30)
151+
pool.close()
152+
pool.join()
153+
154+
# Check that the results are unique
155+
if len(set(aa)) != len(aa) or len(set(res)) != len(res):
156+
raise AssertionError("RNG output duplicated across fork():\n%s" %
157+
(pprint.pformat(res),))
158+
159+
160+
def get_tests(config={}):
161+
tests = []
162+
tests += [RNGForkTest()]
163+
if multiprocessing is not None:
164+
tests += [RNGMultiprocessingForkTest()]
165+
return tests
166+
167+
if __name__ == '__main__':
168+
suite = lambda: unittest.TestSuite(get_tests())
169+
unittest.main(defaultTest='suite')
170+
171+
# vim:set ts=4 sw=4 sts=4 expandtab:

0 commit comments

Comments
 (0)