Skip to content

Commit 360d042

Browse files
committed
Added parallel.py module and ability to use multiprocessing when generating keys
1 parent 70e1555 commit 360d042

6 files changed

Lines changed: 229 additions & 49 deletions

File tree

CHANGELOG.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
Python-RSA changelog
22
========================================
33

4+
Version 3.1 - in development
5+
----------------------------------------
6+
7+
- Added ability to generate keys on multiple cores simultaneously.
8+
9+
410
Version 3.0.1 - released 2011-08-07
511
----------------------------------------
612

create_timing_table.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/usr/bin/env python
2+
3+
import time
4+
import rsa
5+
6+
poolsize = 8
7+
accurate = True
8+
9+
def run_speed_test(bitsize):
10+
11+
iterations = 0
12+
start = end = time.time()
13+
14+
# At least a number of iterations, and at least 2 seconds
15+
while iterations < 10 or end - start < 2:
16+
iterations += 1
17+
rsa.newkeys(bitsize, accurate=accurate, poolsize=poolsize)
18+
end = time.time()
19+
20+
duration = end - start
21+
dur_per_call = duration / iterations
22+
23+
print '%5i bit: %9.3f sec. (%i iterations over %.1f seconds)' % (bitsize,
24+
dur_per_call, iterations, duration)
25+
26+
for bitsize in (128, 256, 384, 512, 1024, 2048, 3072, 4096):
27+
run_speed_test(bitsize)
28+
29+

doc/usage.rst

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -37,42 +37,55 @@ Alternatively you can use :py:meth:`rsa.PrivateKey.load_pkcs1` and
3737
... keydata = privatefile.read()
3838
>>> pubkey = rsa.PrivateKey.load_pkcs1(keydata)
3939

40+
41+
Time to generate a key
42+
++++++++++++++++++++++++++++++++++++++++
43+
4044
Generating a keypair may take a long time, depending on the number of
4145
bits required. The number of bits determines the cryptographic
4246
strength of the key, as well as the size of the message you can
4347
encrypt. If you don't mind having a slightly smaller key than you
4448
requested, you can pass ``accurate=False`` to speed up the key
4549
generation process.
4650

47-
These are some average timings from my netbook (Linux 2.6, 1.6 GHz
48-
Intel Atom N270 CPU, 2 GB RAM). Since key generation is a random
49-
process, times may differ.
50-
51-
+----------------+------------------+
52-
| Keysize (bits) | Time to generate |
53-
+================+==================+
54-
| 32 | 0.01 sec. |
55-
+----------------+------------------+
56-
| 64 | 0.03 sec. |
57-
+----------------+------------------+
58-
| 96 | 0.04 sec. |
59-
+----------------+------------------+
60-
| 128 | 0.08 sec. |
61-
+----------------+------------------+
62-
| 256 | 0.27 sec. |
63-
+----------------+------------------+
64-
| 384 | 0.93 sec. |
65-
+----------------+------------------+
66-
| 512 | 1.21 sec. |
67-
+----------------+------------------+
68-
| 1024 | 7.93 sec. |
69-
+----------------+------------------+
70-
| 2048 | 132.97 sec. |
71-
+----------------+------------------+
51+
Another way to speed up the key generation process is to use multiple
52+
processes in parallel to speed up the key generation. Use no more than
53+
the number of processes that your machine can run in parallel; a
54+
dual-core machine should use ``poolsize=2``; a quad-core
55+
hyperthreading machine can run two threads on each core, and thus can
56+
use ``poolsize=8``.
57+
58+
>>> (pubkey, privkey) = rsa.newkeys(512, poolsize=8)
59+
60+
These are some average timings from my desktop machine (Linux 2.6,
61+
2.93 GHz quad-core Intel Core i7, 16 GB RAM) using 64-bit CPython 2.7.
62+
Since key generation is a random process, times may differ even on
63+
similar hardware. On all tests, we used the default ``accurate=True``.
64+
65+
+----------------+------------------+------------------+
66+
| Keysize (bits) | single process | eight processes |
67+
+================+==================+==================+
68+
| 128 | 0.01 sec. | 0.01 sec. |
69+
+----------------+------------------+------------------+
70+
| 256 | 0.03 sec. | 0.02 sec. |
71+
+----------------+------------------+------------------+
72+
| 384 | 0.09 sec. | 0.04 sec. |
73+
+----------------+------------------+------------------+
74+
| 512 | 0.11 sec. | 0.07 sec. |
75+
+----------------+------------------+------------------+
76+
| 1024 | 0.79 sec. | 0.30 sec. |
77+
+----------------+------------------+------------------+
78+
| 2048 | 6.55 sec. | 1.60 sec. |
79+
+----------------+------------------+------------------+
80+
| 3072 | 23.4 sec. | 7.14 sec. |
81+
+----------------+------------------+------------------+
82+
| 4096 | 72.0 sec. | 24.4 sec. |
83+
+----------------+------------------+------------------+
7284

7385
If key generation is too slow for you, you could use OpenSSL to
74-
generate them for you, then load them in your Python code. See
75-
:ref:`openssl` for more information.
86+
generate them for you, then load them in your Python code. OpenSSL
87+
generates a 4096-bit key in 3.5 seconds on the same machine as used
88+
above. See :ref:`openssl` for more information.
7689

7790
Key size requirements
7891
--------------------------------------------------

rsa/key.py

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -421,15 +421,20 @@ def extended_gcd(a, b):
421421
if (ly < 0): ly += oa #If neg wrap modulo orignal a
422422
return (a, lx, ly) #Return only positive values
423423

424-
def find_p_q(nbits, accurate=True):
424+
def find_p_q(nbits, getprime_func=rsa.prime.getprime, accurate=True):
425425
''''Returns a tuple of two different primes of nbits bits each.
426426
427427
The resulting p * q has exacty 2 * nbits bits, and the returned p and q
428428
will not be equal.
429429
430-
@param nbits: the number of bits in each of p and q.
431-
@param accurate: whether to enable accurate mode or not.
432-
@returns (p, q), where p > q
430+
:param nbits: the number of bits in each of p and q.
431+
:param getprime_func: the getprime function, defaults to
432+
:py:func:`rsa.prime.getprime`.
433+
434+
*Introduced in Python-RSA 3.1*
435+
436+
:param accurate: whether to enable accurate mode or not.
437+
:returns: (p, q), where p > q
433438
434439
>>> (p, q) = find_p_q(128)
435440
>>> from rsa import common
@@ -457,9 +462,9 @@ def find_p_q(nbits, accurate=True):
457462

458463
# Choose the two initial primes
459464
log.debug('find_p_q(%i): Finding p', nbits)
460-
p = rsa.prime.getprime(pbits)
465+
p = getprime_func(pbits)
461466
log.debug('find_p_q(%i): Finding q', nbits)
462-
q = rsa.prime.getprime(qbits)
467+
q = getprime_func(qbits)
463468

464469
def is_acceptable(p, q):
465470
'''Returns True iff p and q are acceptable:
@@ -480,16 +485,12 @@ def is_acceptable(p, q):
480485

481486
# Keep choosing other primes until they match our requirements.
482487
change_p = False
483-
tries = 0
484488
while not is_acceptable(p, q):
485-
tries += 1
486489
# Change p on one iteration and q on the other
487490
if change_p:
488-
log.debug(' find another p')
489-
p = rsa.prime.getprime(pbits)
491+
p = getprime_func(pbits)
490492
else:
491-
log.debug(' find another q')
492-
q = rsa.prime.getprime(qbits)
493+
q = getprime_func(qbits)
493494

494495
change_p = not change_p
495496

@@ -522,41 +523,62 @@ def calculate_keys(p, q, nbits):
522523

523524
return (e, d)
524525

525-
def gen_keys(nbits, accurate=True):
526+
def gen_keys(nbits, getprime_func, accurate=True):
526527
"""Generate RSA keys of nbits bits. Returns (p, q, e, d).
527528
528529
Note: this can take a long time, depending on the key size.
529530
530-
@param nbits: the total number of bits in ``p`` and ``q``. Both ``p`` and
531+
:param nbits: the total number of bits in ``p`` and ``q``. Both ``p`` and
531532
``q`` will use ``nbits/2`` bits.
533+
:param getprime_func: either :py:func:`rsa.prime.getprime` or a function
534+
with similar signature.
532535
"""
533536

534-
(p, q) = find_p_q(nbits // 2, accurate)
537+
(p, q) = find_p_q(nbits // 2, getprime_func, accurate)
535538
(e, d) = calculate_keys(p, q, nbits // 2)
536539

537540
return (p, q, e, d)
538541

539-
def newkeys(nbits, accurate=True):
542+
def newkeys(nbits, accurate=True, poolsize=1):
540543
"""Generates public and private keys, and returns them as (pub, priv).
541544
542545
The public key is also known as the 'encryption key', and is a
543-
:py:class:`PublicKey` object. The private key is also known as the
544-
'decryption key' and is a :py:class:`PrivateKey` object.
546+
:py:class:`rsa.PublicKey` object. The private key is also known as the
547+
'decryption key' and is a :py:class:`rsa.PrivateKey` object.
545548
546549
:param nbits: the number of bits required to store ``n = p*q``.
547550
:param accurate: when True, ``n`` will have exactly the number of bits you
548551
asked for. However, this makes key generation much slower. When False,
549552
`n`` may have slightly less bits.
553+
:param poolsize: the number of processes to use to generate the prime
554+
numbers. If set to a number > 1, a parallel algorithm will be used.
555+
This requires Python 2.6 or newer.
550556
551557
:returns: a tuple (:py:class:`rsa.PublicKey`, :py:class:`rsa.PrivateKey`)
558+
559+
The ``poolsize`` parameter was added in *Python-RSA 3.1* and requires
560+
Python 2.6 or newer.
552561
553562
"""
554563

555564
if nbits < 16:
556565
raise ValueError('Key too small')
557566

558-
(p, q, e, d) = gen_keys(nbits)
567+
if poolsize < 1:
568+
raise ValueError('Pool size (%i) should be >= 1' % poolsize)
569+
570+
# Determine which getprime function to use
571+
if poolsize > 1:
572+
from rsa import parallel
573+
import functools
574+
575+
getprime_func = functools.partial(parallel.getprime, poolsize=poolsize)
576+
else: getprime_func = rsa.prime.getprime
577+
578+
# Generate the key components
579+
(p, q, e, d) = gen_keys(nbits, getprime_func)
559580

581+
# Create the key objects
560582
n = p * q
561583

562584
return (

rsa/parallel.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright 2011 Sybren A. Stüvel <sybren@stuvel.eu>
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
'''Functions for parallel computation on multiple cores.
18+
19+
Introduced in Python-RSA 3.1.
20+
21+
.. note::
22+
23+
Requires Python 2.6 or newer.
24+
25+
'''
26+
27+
import multiprocessing as mp
28+
29+
import rsa.prime
30+
import rsa.randnum
31+
32+
def _find_prime(nbits, pipe):
33+
while True:
34+
integer = rsa.randnum.read_random_int(nbits)
35+
36+
# Make sure it's odd
37+
integer |= 1
38+
39+
# Test for primeness
40+
if rsa.prime.is_prime(integer):
41+
pipe.send(integer)
42+
return
43+
44+
def getprime(nbits, poolsize):
45+
"""Returns a prime number that can be stored in 'nbits' bits.
46+
47+
Works in multiple threads at the same time.
48+
49+
>>> p = getprime(128, 3)
50+
>>> rsa.prime.is_prime(p-1)
51+
False
52+
>>> rsa.prime.is_prime(p)
53+
True
54+
>>> rsa.prime.is_prime(p+1)
55+
False
56+
57+
>>> from rsa import common
58+
>>> common.bit_size(p) == 128
59+
True
60+
61+
"""
62+
63+
(pipe_recv, pipe_send) = mp.Pipe(duplex=False)
64+
65+
# Create processes
66+
procs = [mp.Process(target=_find_prime, args=(nbits, pipe_send))
67+
for _ in range(poolsize)]
68+
[p.start() for p in procs]
69+
70+
result = pipe_recv.recv()
71+
72+
[p.terminate() for p in procs]
73+
74+
return result
75+
76+
__all__ = ['getprime']
77+
78+
79+
if __name__ == '__main__':
80+
print 'Running doctests 1000x or until failure'
81+
import doctest
82+
83+
for count in range(100):
84+
(failures, tests) = doctest.testmod()
85+
if failures:
86+
break
87+
88+
if count and count % 10 == 0:
89+
print '%i times' % count
90+
91+
print 'Doctests done'
92+

0 commit comments

Comments
 (0)