-
Notifications
You must be signed in to change notification settings - Fork 240
/
Copy pathrsa_backend.py
283 lines (239 loc) · 10.7 KB
/
rsa_backend.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
import binascii
import warnings
import rsa as pyrsa
import rsa.pem as pyrsa_pem
from pyasn1.error import PyAsn1Error
from rsa import DecryptionError
from jose.backends._asn1 import (
rsa_private_key_pkcs1_to_pkcs8,
rsa_private_key_pkcs8_to_pkcs1,
rsa_public_key_pkcs1_to_pkcs8,
)
from jose.backends.base import Key
from jose.constants import ALGORITHMS
from jose.exceptions import JWEError, JWKError
from jose.utils import base64_to_long, long_to_base64
ALGORITHMS.SUPPORTED.remove(ALGORITHMS.RSA_OAEP) # RSA OAEP not supported
LEGACY_INVALID_PKCS8_RSA_HEADER = binascii.unhexlify(
"30" # sequence
"8204BD" # DER-encoded sequence contents length of 1213 bytes -- INCORRECT STATIC LENGTH
"020100" # integer: 0 -- Version
"30" # sequence
"0D" # DER-encoded sequence contents length of 13 bytes -- PrivateKeyAlgorithmIdentifier
"06092A864886F70D010101" # OID -- rsaEncryption
"0500" # NULL -- parameters
)
ASN1_SEQUENCE_ID = binascii.unhexlify("30")
RSA_ENCRYPTION_ASN1_OID = "1.2.840.113549.1.1.1"
# Functions gcd and rsa_recover_prime_factors were copied from cryptography 1.9
# to enable pure python rsa module to be in compliance with section 6.3.1 of RFC7518
# which requires only private exponent (d) for private key.
def _gcd(a, b):
"""Calculate the Greatest Common Divisor of a and b.
Unless b==0, the result will have the same sign as b (so that when
b is divided by it, the result comes out positive).
"""
while b:
a, b = b, (a % b)
return a
# Controls the number of iterations rsa_recover_prime_factors will perform
# to obtain the prime factors. Each iteration increments by 2 so the actual
# maximum attempts is half this number.
_MAX_RECOVERY_ATTEMPTS = 1000
def _rsa_recover_prime_factors(n, e, d):
"""
Compute factors p and q from the private exponent d. We assume that n has
no more than two factors. This function is adapted from code in PyCrypto.
"""
# See 8.2.2(i) in Handbook of Applied Cryptography.
ktot = d * e - 1
# The quantity d*e-1 is a multiple of phi(n), even,
# and can be represented as t*2^s.
t = ktot
while t % 2 == 0:
t = t // 2
# Cycle through all multiplicative inverses in Zn.
# The algorithm is non-deterministic, but there is a 50% chance
# any candidate a leads to successful factoring.
# See "Digitalized Signatures and Public Key Functions as Intractable
# as Factorization", M. Rabin, 1979
spotted = False
a = 2
while not spotted and a < _MAX_RECOVERY_ATTEMPTS:
k = t
# Cycle through all values a^{t*2^i}=a^k
while k < ktot:
cand = pow(a, k, n)
# Check if a^k is a non-trivial root of unity (mod n)
if cand != 1 and cand != (n - 1) and pow(cand, 2, n) == 1:
# We have found a number such that (cand-1)(cand+1)=0 (mod n).
# Either of the terms divides n.
p = _gcd(cand + 1, n)
spotted = True
break
k *= 2
# This value was not any good... let's try another!
a += 2
if not spotted:
raise ValueError("Unable to compute factors p and q from exponent d.")
# Found !
q, r = divmod(n, p)
assert r == 0
p, q = sorted((p, q), reverse=True)
return (p, q)
def pem_to_spki(pem, fmt="PKCS8"):
key = RSAKey(pem, ALGORITHMS.RS256)
return key.to_pem(fmt)
def _legacy_private_key_pkcs8_to_pkcs1(pkcs8_key):
"""Legacy RSA private key PKCS8-to-PKCS1 conversion.
.. warning::
This is incorrect parsing and only works because the legacy PKCS1-to-PKCS8
encoding was also incorrect.
"""
# Only allow this processing if the prefix matches
# AND the following byte indicates an ASN1 sequence,
# as we would expect with the legacy encoding.
if not pkcs8_key.startswith(LEGACY_INVALID_PKCS8_RSA_HEADER + ASN1_SEQUENCE_ID):
raise ValueError("Invalid private key encoding")
return pkcs8_key[len(LEGACY_INVALID_PKCS8_RSA_HEADER) :]
class RSAKey(Key):
SHA256 = "SHA-256"
SHA384 = "SHA-384"
SHA512 = "SHA-512"
def __init__(self, key, algorithm):
if algorithm not in ALGORITHMS.RSA:
raise JWKError("hash_alg: %s is not a valid hash algorithm" % algorithm)
if algorithm in ALGORITHMS.RSA_KW and algorithm != ALGORITHMS.RSA1_5:
raise JWKError("alg: %s is not supported by the RSA backend" % algorithm)
self.hash_alg = {
ALGORITHMS.RS256: self.SHA256,
ALGORITHMS.RS384: self.SHA384,
ALGORITHMS.RS512: self.SHA512,
}.get(algorithm)
self._algorithm = algorithm
if isinstance(key, dict):
self._prepared_key = self._process_jwk(key)
return
if isinstance(key, (pyrsa.PublicKey, pyrsa.PrivateKey)):
self._prepared_key = key
return
if isinstance(key, str):
key = key.encode("utf-8")
if isinstance(key, bytes):
try:
self._prepared_key = pyrsa.PublicKey.load_pkcs1(key)
except ValueError:
try:
self._prepared_key = pyrsa.PublicKey.load_pkcs1_openssl_pem(key)
except ValueError:
try:
self._prepared_key = pyrsa.PrivateKey.load_pkcs1(key)
except ValueError:
try:
der = pyrsa_pem.load_pem(key, b"PRIVATE KEY")
try:
pkcs1_key = rsa_private_key_pkcs8_to_pkcs1(der)
except PyAsn1Error:
# If the key was encoded using the old, invalid,
# encoding then pyasn1 will throw an error attempting
# to parse the key.
pkcs1_key = _legacy_private_key_pkcs8_to_pkcs1(der)
self._prepared_key = pyrsa.PrivateKey.load_pkcs1(pkcs1_key, format="DER")
except ValueError as e:
raise JWKError(e)
return
raise JWKError("Unable to parse an RSA_JWK from key: %s" % key)
def _process_jwk(self, jwk_dict):
if not jwk_dict.get("kty") == "RSA":
raise JWKError("Incorrect key type. Expected: 'RSA', Received: %s" % jwk_dict.get("kty"))
e = base64_to_long(jwk_dict.get("e"))
n = base64_to_long(jwk_dict.get("n"))
if "d" not in jwk_dict:
return pyrsa.PublicKey(e=e, n=n)
else:
d = base64_to_long(jwk_dict.get("d"))
extra_params = ["p", "q", "dp", "dq", "qi"]
if any(k in jwk_dict for k in extra_params):
# Precomputed private key parameters are available.
if not all(k in jwk_dict for k in extra_params):
# These values must be present when 'p' is according to
# Section 6.3.2 of RFC7518, so if they are not we raise
# an error.
raise JWKError("Precomputed private key parameters are incomplete.")
p = base64_to_long(jwk_dict["p"])
q = base64_to_long(jwk_dict["q"])
return pyrsa.PrivateKey(e=e, n=n, d=d, p=p, q=q)
else:
p, q = _rsa_recover_prime_factors(n, e, d)
return pyrsa.PrivateKey(n=n, e=e, d=d, p=p, q=q)
def sign(self, msg):
return pyrsa.sign(msg, self._prepared_key, self.hash_alg)
def verify(self, msg, sig):
if not self.is_public():
warnings.warn("Attempting to verify a message with a private key. " "This is not recommended.")
try:
pyrsa.verify(msg, sig, self._prepared_key)
return True
except pyrsa.pkcs1.VerificationError:
return False
def is_public(self):
return isinstance(self._prepared_key, pyrsa.PublicKey)
def public_key(self):
if isinstance(self._prepared_key, pyrsa.PublicKey):
return self
return self.__class__(pyrsa.PublicKey(n=self._prepared_key.n, e=self._prepared_key.e), self._algorithm)
def to_pem(self, pem_format="PKCS8"):
if isinstance(self._prepared_key, pyrsa.PrivateKey):
der = self._prepared_key.save_pkcs1(format="DER")
if pem_format == "PKCS8":
pkcs8_der = rsa_private_key_pkcs1_to_pkcs8(der)
pem = pyrsa_pem.save_pem(pkcs8_der, pem_marker="PRIVATE KEY")
elif pem_format == "PKCS1":
pem = pyrsa_pem.save_pem(der, pem_marker="RSA PRIVATE KEY")
else:
raise ValueError(f"Invalid pem format specified: {pem_format!r}")
else:
if pem_format == "PKCS8":
pkcs1_der = self._prepared_key.save_pkcs1(format="DER")
pkcs8_der = rsa_public_key_pkcs1_to_pkcs8(pkcs1_der)
pem = pyrsa_pem.save_pem(pkcs8_der, pem_marker="PUBLIC KEY")
elif pem_format == "PKCS1":
der = self._prepared_key.save_pkcs1(format="DER")
pem = pyrsa_pem.save_pem(der, pem_marker="RSA PUBLIC KEY")
else:
raise ValueError(f"Invalid pem format specified: {pem_format!r}")
return pem
def to_dict(self):
if not self.is_public():
public_key = self.public_key()._prepared_key
else:
public_key = self._prepared_key
data = {
"alg": self._algorithm,
"kty": "RSA",
"n": long_to_base64(public_key.n).decode("ASCII"),
"e": long_to_base64(public_key.e).decode("ASCII"),
}
if not self.is_public():
data.update(
{
"d": long_to_base64(self._prepared_key.d).decode("ASCII"),
"p": long_to_base64(self._prepared_key.p).decode("ASCII"),
"q": long_to_base64(self._prepared_key.q).decode("ASCII"),
"dp": long_to_base64(self._prepared_key.exp1).decode("ASCII"),
"dq": long_to_base64(self._prepared_key.exp2).decode("ASCII"),
"qi": long_to_base64(self._prepared_key.coef).decode("ASCII"),
}
)
return data
def wrap_key(self, key_data):
if not self.is_public():
warnings.warn("Attempting to encrypt a message with a private key." " This is not recommended.")
wrapped_key = pyrsa.encrypt(key_data, self._prepared_key)
return wrapped_key
def unwrap_key(self, wrapped_key):
try:
unwrapped_key = pyrsa.decrypt(wrapped_key, self._prepared_key)
except DecryptionError as e:
raise JWEError(e)
return unwrapped_key