Skip to content

Commit fa34679

Browse files
committed
Added loading of DER and PEM encoded private keys
1 parent 834ae62 commit fa34679

3 files changed

Lines changed: 188 additions & 14 deletions

File tree

rsa/key.py

Lines changed: 81 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
'''
77

88
import rsa.prime
9+
import rsa.pem
910

1011
class PublicKey(object):
1112
'''Represents a public RSA key.
@@ -75,19 +76,6 @@ class PrivateKey(object):
7576
7677
'''
7778

78-
# RSAPrivateKey ::= SEQUENCE {
79-
# version Version,
80-
# modulus INTEGER, -- n
81-
# publicExponent INTEGER, -- e
82-
# privateExponent INTEGER, -- d
83-
# prime1 INTEGER, -- p
84-
# prime2 INTEGER, -- q
85-
# exponent1 INTEGER, -- d mod (p-1)
86-
# exponent2 INTEGER, -- d mod (q-1)
87-
# coefficient INTEGER, -- (inverse of q) mod p
88-
# otherPrimeInfos OtherPrimeInfos OPTIONAL
89-
# }
90-
9179
__slots__ = ('n', 'e', 'd', 'p', 'q', 'exp1', 'exp2', 'coef')
9280

9381
def __init__(self, n, e, d, p, q, exp1=None, exp2=None, coef=None):
@@ -119,6 +107,24 @@ def __getitem__(self, key):
119107
def __repr__(self):
120108
return u'PrivateKey(%(n)i, %(e)i, %(d)i, %(p)i, %(q)i)' % self
121109

110+
def __eq__(self, other):
111+
if other is None:
112+
return False
113+
114+
if not isinstance(other, PrivateKey):
115+
return False
116+
117+
return (self.n == other.n and
118+
self.e == other.e and
119+
self.d == other.d and
120+
self.p == other.p and
121+
self.q == other.q and
122+
self.exp1 == other.exp1 and
123+
self.exp2 == other.exp2 and
124+
self.coef == other.coef)
125+
126+
def __ne__(self, other):
127+
return not (self == other)
122128

123129
def extended_gcd(a, b):
124130
"""Returns a tuple (r, i, j) such that r = gcd(a, b) = ia + jb
@@ -234,7 +240,68 @@ def newkeys(nbits):
234240
PrivateKey(n, e, d, p, q)
235241
)
236242

237-
__all__ = ['PublicKey', 'PrivateKey', 'newkeys']
243+
def load_private_key_der(keyfile):
244+
r'''Loads a key in DER format.
245+
246+
@param keyfile: contents of a DER-encoded file that contains the private
247+
key.
248+
@return: a PrivateKey object
249+
250+
First let's construct a DER encoded key:
251+
252+
>>> import base64
253+
>>> b64der = 'MC4CAQACBQDeKYlRAgMBAAECBQDHn4npAgMA/icCAwDfxwIDANcXAgInbwIDAMZt'
254+
>>> der = base64.decodestring(b64der)
255+
256+
This loads the file:
257+
258+
>>> load_private_key_der(der)
259+
PrivateKey(3727264081, 65537, 3349121513, 65063, 57287)
260+
261+
'''
262+
263+
from pyasn1.codec.der import decoder
264+
(priv, _) = decoder.decode(keyfile)
265+
266+
# ASN.1 contents of DER encoded private key:
267+
#
268+
# RSAPrivateKey ::= SEQUENCE {
269+
# version Version,
270+
# modulus INTEGER, -- n
271+
# publicExponent INTEGER, -- e
272+
# privateExponent INTEGER, -- d
273+
# prime1 INTEGER, -- p
274+
# prime2 INTEGER, -- q
275+
# exponent1 INTEGER, -- d mod (p-1)
276+
# exponent2 INTEGER, -- d mod (q-1)
277+
# coefficient INTEGER, -- (inverse of q) mod p
278+
# otherPrimeInfos OtherPrimeInfos OPTIONAL
279+
# }
280+
281+
if priv[0] != 0:
282+
raise ValueError('Unable to read this file, version %s != 0' % priv[0])
283+
284+
return PrivateKey(*priv[1:9])
285+
286+
def load_private_key_pem(keyfile):
287+
'''Loads a PEM-encoded private key file.
288+
289+
The contents of the file before the "-----BEGIN RSA PRIVATE KEY-----" and
290+
after the "-----END RSA PRIVATE KEY-----" lines is ignored.
291+
292+
@param keyfile: contents of a PEM-encoded file that contains the private
293+
key.
294+
@return: a PrivateKey object
295+
'''
296+
297+
PEM_START = '-----BEGIN RSA PRIVATE KEY-----'
298+
PEM_END = '-----END RSA PRIVATE KEY-----'
299+
300+
der = rsa.pem.load_pem(keyfile, PEM_START, PEM_END)
301+
return load_private_key_der(der)
302+
303+
304+
__all__ = ['PublicKey', 'PrivateKey', 'newkeys', 'load']
238305

239306
if __name__ == '__main__':
240307
import doctest

rsa/pem.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'''Functions that load and write PEM-encoded files.'''
2+
3+
import base64
4+
5+
def load_pem(contents, pem_start, pem_end):
6+
'''Loads a PEM file.
7+
8+
Only considers the information between lines "pem_start" and "pem_end". For
9+
private keys these are '-----BEGIN RSA PRIVATE KEY-----' and
10+
'-----END RSA PRIVATE KEY-----'
11+
12+
@param contents: the contents of the file to interpret
13+
@param pem_start: the start marker of the PEM content, such as
14+
'-----BEGIN RSA PRIVATE KEY-----'
15+
@param pem_end: the end marker of the PEM content, such as
16+
'-----END RSA PRIVATE KEY-----'
17+
18+
@return the base64-decoded content between the start and end markers.
19+
20+
@raise ValueError: when the content is invalid, for example when the start
21+
marker cannot be found.
22+
23+
'''
24+
25+
pem_lines = []
26+
in_pem_part = False
27+
28+
for line in contents.split('\n'):
29+
line = line.strip()
30+
31+
# Handle start marker
32+
if line == pem_start:
33+
if in_pem_part:
34+
raise ValueError('Seen start marker "%s" twice' % pem_start)
35+
36+
in_pem_part = True
37+
continue
38+
39+
# Skip stuff before first marker
40+
if not in_pem_part:
41+
continue
42+
43+
# Handle end marker
44+
if in_pem_part and line == pem_end:
45+
in_pem_part = False
46+
break
47+
48+
pem_lines.append(line)
49+
50+
# Do some sanity checks
51+
if not pem_lines:
52+
raise ValueError('No PEM start marker "%s" found' % pem_start)
53+
54+
if in_pem_part:
55+
raise ValueError('No PEM end marker "%s" found' % pem_end)
56+
57+
# Base64-decode the contents
58+
pem = ''.join(pem_lines)
59+
return base64.decodestring(pem)
60+

tests/test_load_save_keys.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'''Unittest for saving and loading keys.'''
2+
3+
import base64
4+
import unittest
5+
6+
import rsa.key
7+
8+
B64PRIV_DER = 'MC4CAQACBQDeKYlRAgMBAAECBQDHn4npAgMA/icCAwDfxwIDANcXAgInbwIDAMZt'
9+
PRIVATE_DER = base64.decodestring(B64PRIV_DER)
10+
11+
PRIVATE_PEM = '''
12+
-----BEGIN CONFUSING STUFF-----
13+
Cruft before the key
14+
15+
-----BEGIN RSA PRIVATE KEY-----
16+
%s
17+
-----END RSA PRIVATE KEY-----
18+
19+
Stuff after the key
20+
-----END CONFUSING STUFF-----
21+
''' % B64PRIV_DER
22+
23+
24+
class DerTest(unittest.TestCase):
25+
'''Test saving and loading DER keys.'''
26+
27+
def test_load_private_key(self):
28+
'''Test loading private DER keys.'''
29+
30+
key = rsa.key.load_private_key_der(PRIVATE_DER)
31+
expected = rsa.key.PrivateKey(3727264081, 65537, 3349121513, 65063, 57287)
32+
33+
self.assertEqual(expected, key)
34+
35+
36+
class PemTest(unittest.TestCase):
37+
'''Test saving and loading PEM keys.'''
38+
39+
40+
def test_load_private_key(self):
41+
'''Test loading private PEM files.'''
42+
43+
key = rsa.key.load_private_key_pem(PRIVATE_PEM)
44+
expected = rsa.key.PrivateKey(3727264081, 65537, 3349121513, 65063, 57287)
45+
46+
self.assertEqual(expected, key)
47+

0 commit comments

Comments
 (0)