Skip to content

Commit c2291b1

Browse files
committed
Implement support with PyCryptodome
1 parent 20add39 commit c2291b1

File tree

10 files changed

+310
-31
lines changed

10 files changed

+310
-31
lines changed
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import Cryptodome.Hash.SHA256
2+
import Cryptodome.Hash.SHA384
3+
import Cryptodome.Hash.SHA512
4+
from Cryptodome.PublicKey import ECC, RSA
5+
from Cryptodome.Signature import DSS, PKCS1_v1_5
6+
7+
from jwt.algorithms import Algorithm
8+
from jwt.compat import string_types, text_type
9+
10+
11+
class RSAAlgorithm(Algorithm):
12+
"""
13+
Performs signing and verification operations using
14+
RSASSA-PKCS-v1_5 and the specified hash function.
15+
16+
This class requires PyCryptodome package to be installed.
17+
"""
18+
19+
SHA256 = Cryptodome.Hash.SHA256
20+
SHA384 = Cryptodome.Hash.SHA384
21+
SHA512 = Cryptodome.Hash.SHA512
22+
23+
def __init__(self, hash_alg):
24+
self.hash_alg = hash_alg
25+
26+
def prepare_key(self, key):
27+
28+
if isinstance(key, RSA.RsaKey):
29+
return key
30+
31+
if isinstance(key, string_types):
32+
if isinstance(key, text_type):
33+
key = key.encode("utf-8")
34+
35+
key = RSA.importKey(key)
36+
else:
37+
raise TypeError("Expecting a PEM- or RSA-formatted key.")
38+
39+
return key
40+
41+
def sign(self, msg, key):
42+
return PKCS1_v1_5.new(key).sign(self.hash_alg.new(msg))
43+
44+
def verify(self, msg, key, sig):
45+
return PKCS1_v1_5.new(key).verify(self.hash_alg.new(msg), sig)
46+
47+
48+
class ECAlgorithm(Algorithm):
49+
"""
50+
Performs signing and verification operations using
51+
ECDSA and the specified hash function
52+
53+
This class requires the PyCryptodome package to be installed.
54+
"""
55+
56+
SHA256 = Cryptodome.Hash.SHA256
57+
SHA384 = Cryptodome.Hash.SHA384
58+
SHA512 = Cryptodome.Hash.SHA512
59+
60+
def __init__(self, hash_alg):
61+
self.hash_alg = hash_alg
62+
63+
def prepare_key(self, key):
64+
65+
if isinstance(key, ECC.EccKey):
66+
return key
67+
68+
if isinstance(key, string_types):
69+
if isinstance(key, text_type):
70+
key = key.encode("utf-8")
71+
key = ECC.import_key(key)
72+
else:
73+
raise TypeError("Expecting a PEM- or ECC-formatted key.")
74+
75+
return key
76+
77+
def sign(self, msg, key):
78+
signer = DSS.new(key, "fips-186-3")
79+
hash_obj = self.hash_alg.new(msg)
80+
return signer.sign(hash_obj)
81+
82+
def verify(self, msg, key, sig):
83+
verifier = DSS.new(key, "fips-186-3")
84+
hash_obj = self.hash_alg.new(msg)
85+
86+
try:
87+
verifier.verify(hash_obj, sig)
88+
return True
89+
except ValueError:
90+
return False

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ use_parentheses=true
1111
combine_as_imports=true
1212

1313
known_first_party="jwt"
14-
known_third_party=["Crypto", "ecdsa", "pytest", "setuptools", "sphinx_rtd_theme"]
14+
known_third_party=["Crypto", "Cryptodome", "ecdsa", "pytest", "setuptools", "sphinx_rtd_theme"]

setup.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,15 @@ def get_version(package):
3535
EXTRAS_REQUIRE = {
3636
"jwks-client": ["requests"],
3737
"tests": ["pytest>=4.0.1,<5.0.0", "pytest-cov>=2.6.0,<3.0.0"],
38-
"crypto": ["cryptography >= 1.4"],
38+
"cryptography": ["cryptography >= 1.4"],
39+
"pycryptodome": ["pycryptodomex"],
3940
}
4041

4142
EXTRAS_REQUIRE["dev"] = (
4243
EXTRAS_REQUIRE["tests"]
43-
+ EXTRAS_REQUIRE["crypto"]
44+
+ EXTRAS_REQUIRE["cryptography"]
4445
+ EXTRAS_REQUIRE["jwks-client"]
46+
+ EXTRAS_REQUIRE["pycryptodome"]
4547
+ ["mypy", "pre-commit"]
4648
)
4749

tests/contrib/test_algorithms.py

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@
2020
except ImportError:
2121
has_ecdsa = False
2222

23+
try:
24+
# fmt: off
25+
from jwt.contrib.algorithms.pycryptodome import RSAAlgorithm, ECAlgorithm # noqa: F811
26+
# fmt: on
27+
28+
has_pycryptodome = True
29+
except ImportError:
30+
has_pycryptodome = False
31+
2332

2433
@pytest.mark.skipif(
2534
not has_pycrypto, reason="Not supported without PyCrypto library"
@@ -212,3 +221,188 @@ def test_ec_prepare_key_should_be_idempotent(self):
212221
jwt_pub_key_second = algo.prepare_key(jwt_pub_key_first)
213222

214223
assert jwt_pub_key_first == jwt_pub_key_second
224+
225+
226+
@pytest.mark.skipif(
227+
not has_pycryptodome, reason="Not supported without PyCryptodome library"
228+
)
229+
class TestPyCryptodomeAlgorithms:
230+
def test_rsa_should_parse_pem_public_key(self):
231+
algo = RSAAlgorithm(RSAAlgorithm.SHA256)
232+
233+
with open(key_path("testkey2_rsa.pub.pem"), "r") as pem_key:
234+
algo.prepare_key(pem_key.read())
235+
236+
def test_rsa_should_accept_unicode_key(self):
237+
algo = RSAAlgorithm(RSAAlgorithm.SHA256)
238+
239+
with open(key_path("testkey_rsa"), "r") as rsa_key:
240+
algo.prepare_key(force_unicode(rsa_key.read()))
241+
242+
def test_rsa_should_reject_non_string_key(self):
243+
algo = RSAAlgorithm(RSAAlgorithm.SHA256)
244+
245+
with pytest.raises(TypeError):
246+
algo.prepare_key(None)
247+
248+
def test_rsa_sign_should_generate_correct_signature_value(self):
249+
algo = RSAAlgorithm(RSAAlgorithm.SHA256)
250+
251+
jwt_message = force_bytes("Hello World!")
252+
253+
expected_sig = base64.b64decode(
254+
force_bytes(
255+
"yS6zk9DBkuGTtcBzLUzSpo9gGJxJFOGvUqN01iLhWHrzBQ9ZEz3+Ae38AXp"
256+
"10RWwscp42ySC85Z6zoN67yGkLNWnfmCZSEv+xqELGEvBJvciOKsrhiObUl"
257+
"2mveSc1oeO/2ujkGDkkkJ2epn0YliacVjZF5+/uDmImUfAAj8lzjnHlzYix"
258+
"sn5jGz1H07jYYbi9diixN8IUhXeTafwFg02IcONhum29V40Wu6O5tAKWlJX"
259+
"fHJnNUzAEUOXS0WahHVb57D30pcgIji9z923q90p5c7E2cU8V+E1qe8NdCA"
260+
"APCDzZZ9zQ/dgcMVaBrGrgimrcLbPjueOKFgSO+SSjIElKA=="
261+
)
262+
)
263+
264+
with open(key_path("testkey_rsa"), "r") as keyfile:
265+
jwt_key = algo.prepare_key(keyfile.read())
266+
267+
with open(key_path("testkey_rsa.pub"), "r") as keyfile:
268+
jwt_pub_key = algo.prepare_key(keyfile.read())
269+
270+
algo.sign(jwt_message, jwt_key)
271+
result = algo.verify(jwt_message, jwt_pub_key, expected_sig)
272+
assert result
273+
274+
def test_rsa_verify_should_return_false_if_signature_invalid(self):
275+
algo = RSAAlgorithm(RSAAlgorithm.SHA256)
276+
277+
jwt_message = force_bytes("Hello World!")
278+
279+
jwt_sig = base64.b64decode(
280+
force_bytes(
281+
"yS6zk9DBkuGTtcBzLUzSpo9gGJxJFOGvUqN01iLhWHrzBQ9ZEz3+Ae38AXp"
282+
"10RWwscp42ySC85Z6zoN67yGkLNWnfmCZSEv+xqELGEvBJvciOKsrhiObUl"
283+
"2mveSc1oeO/2ujkGDkkkJ2epn0YliacVjZF5+/uDmImUfAAj8lzjnHlzYix"
284+
"sn5jGz1H07jYYbi9diixN8IUhXeTafwFg02IcONhum29V40Wu6O5tAKWlJX"
285+
"fHJnNUzAEUOXS0WahHVb57D30pcgIji9z923q90p5c7E2cU8V+E1qe8NdCA"
286+
"APCDzZZ9zQ/dgcMVaBrGrgimrcLbPjueOKFgSO+SSjIElKA=="
287+
)
288+
)
289+
290+
jwt_sig += force_bytes("123") # Signature is now invalid
291+
292+
with open(key_path("testkey_rsa.pub"), "r") as keyfile:
293+
jwt_pub_key = algo.prepare_key(keyfile.read())
294+
295+
result = algo.verify(jwt_message, jwt_pub_key, jwt_sig)
296+
assert not result
297+
298+
def test_rsa_verify_should_return_true_if_signature_valid(self):
299+
algo = RSAAlgorithm(RSAAlgorithm.SHA256)
300+
301+
jwt_message = force_bytes("Hello World!")
302+
303+
jwt_sig = base64.b64decode(
304+
force_bytes(
305+
"yS6zk9DBkuGTtcBzLUzSpo9gGJxJFOGvUqN01iLhWHrzBQ9ZEz3+Ae38AXp"
306+
"10RWwscp42ySC85Z6zoN67yGkLNWnfmCZSEv+xqELGEvBJvciOKsrhiObUl"
307+
"2mveSc1oeO/2ujkGDkkkJ2epn0YliacVjZF5+/uDmImUfAAj8lzjnHlzYix"
308+
"sn5jGz1H07jYYbi9diixN8IUhXeTafwFg02IcONhum29V40Wu6O5tAKWlJX"
309+
"fHJnNUzAEUOXS0WahHVb57D30pcgIji9z923q90p5c7E2cU8V+E1qe8NdCA"
310+
"APCDzZZ9zQ/dgcMVaBrGrgimrcLbPjueOKFgSO+SSjIElKA=="
311+
)
312+
)
313+
314+
with open(key_path("testkey_rsa.pub"), "r") as keyfile:
315+
jwt_pub_key = algo.prepare_key(keyfile.read())
316+
317+
result = algo.verify(jwt_message, jwt_pub_key, jwt_sig)
318+
assert result
319+
320+
def test_rsa_prepare_key_should_be_idempotent(self):
321+
algo = RSAAlgorithm(RSAAlgorithm.SHA256)
322+
323+
with open(key_path("testkey_rsa.pub"), "r") as keyfile:
324+
jwt_pub_key_first = algo.prepare_key(keyfile.read())
325+
jwt_pub_key_second = algo.prepare_key(jwt_pub_key_first)
326+
327+
assert jwt_pub_key_first == jwt_pub_key_second
328+
329+
def test_ec_should_reject_non_string_key(self):
330+
algo = ECAlgorithm(ECAlgorithm.SHA256)
331+
332+
with pytest.raises(TypeError):
333+
algo.prepare_key(None)
334+
335+
def test_ec_should_accept_unicode_key(self):
336+
algo = ECAlgorithm(ECAlgorithm.SHA256)
337+
338+
with open(key_path("testkey_ec"), "r") as ec_key:
339+
algo.prepare_key(force_unicode(ec_key.read()))
340+
341+
def test_ec_sign_should_generate_correct_signature_value(self):
342+
algo = ECAlgorithm(ECAlgorithm.SHA256)
343+
344+
jwt_message = force_bytes("Hello World!")
345+
346+
expected_sig = base64.b64decode(
347+
force_bytes(
348+
"v163MdgPbEj2C/ZPRvPtLBtKaKitqZ4wg0Vs7mpoQb+i18flD"
349+
"eKb19cZn7h1E3tuP1CgthJvvIBdRKQf0OXlZA=="
350+
)
351+
)
352+
353+
with open(key_path("testkey_ec"), "r") as keyfile:
354+
jwt_key = algo.prepare_key(keyfile.read())
355+
356+
with open(key_path("testkey_ec.pub"), "r") as keyfile:
357+
jwt_pub_key = algo.prepare_key(keyfile.read())
358+
359+
algo.sign(jwt_message, jwt_key)
360+
result = algo.verify(jwt_message, jwt_pub_key, expected_sig)
361+
assert result
362+
363+
def test_ec_verify_should_return_false_if_signature_invalid(self):
364+
algo = ECAlgorithm(ECAlgorithm.SHA256)
365+
366+
jwt_message = force_bytes("Hello World!")
367+
368+
jwt_sig = base64.b64decode(
369+
force_bytes(
370+
"v163MdgPbEj2C/ZPRvPtLBtKaKitqZ4wg0Vs7mpoQb+i18flD"
371+
"eKb19cZn7h1E3tuP1CgthJvvIBdRKQf0OXlZA=="
372+
)
373+
)
374+
375+
jwt_sig += force_bytes("123") # Signature is now invalid
376+
377+
with open(key_path("testkey_ec.pub"), "r") as keyfile:
378+
jwt_pub_key = algo.prepare_key(keyfile.read())
379+
380+
result = algo.verify(jwt_message, jwt_pub_key, jwt_sig)
381+
assert not result
382+
383+
def test_ec_verify_should_return_true_if_signature_valid(self):
384+
algo = ECAlgorithm(ECAlgorithm.SHA256)
385+
386+
jwt_message = force_bytes("Hello World!")
387+
388+
jwt_sig = base64.b64decode(
389+
force_bytes(
390+
"v163MdgPbEj2C/ZPRvPtLBtKaKitqZ4wg0Vs7mpoQb+i18flD"
391+
"eKb19cZn7h1E3tuP1CgthJvvIBdRKQf0OXlZA=="
392+
)
393+
)
394+
395+
with open(key_path("testkey_ec.pub"), "r") as keyfile:
396+
jwt_pub_key = algo.prepare_key(keyfile.read())
397+
398+
result = algo.verify(jwt_message, jwt_pub_key, jwt_sig)
399+
assert result
400+
401+
def test_ec_prepare_key_should_be_idempotent(self):
402+
algo = ECAlgorithm(ECAlgorithm.SHA256)
403+
404+
with open(key_path("testkey_ec.pub"), "r") as keyfile:
405+
jwt_pub_key_first = algo.prepare_key(keyfile.read())
406+
jwt_pub_key_second = algo.prepare_key(jwt_pub_key_first)
407+
408+
assert jwt_pub_key_first == jwt_pub_key_second

tests/keys/testkey_ec

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
-----BEGIN EC PRIVATE KEY-----
2-
MIHbAgEBBEG4xN/z6gk7bPkEzs1hHOsbs+Gi2lku8YH4LkS4E1q9U9jSOjvEcFNH
3-
m/CQjKi1rtpAb0/WL3p/wXsc26e7zmAA5KAHBgUrgQQAI6GBiQOBhgAEAVnCcDxA
4-
J0v5OJBYFIcTReydEkEIWRvpzYMvv5l8IUOT2SFJiHdWtU45DV4is7+g6bbQanbh
5-
28/1dBLR/kH1stAeAYWeTJ08gxo3M9Q0KinXsXm4c6G24UiGY6WHeWlOPKPa16fz
6-
pwJ62o3XaRrCdGzX+K7TCwahWCTeizrJQAe8UwUY
7-
-----END EC PRIVATE KEY-----
1+
-----BEGIN PRIVATE KEY-----
2+
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg2nninfu2jMHDwAbn
3+
9oERUhRADS6duQaJEadybLaa0YShRANCAAQfMBxRZKUYEdy5/fLdGI2tYj6kTr50
4+
PZPt8jOD23rAR7dhtNpG1ojqopmH0AH5wEXadgk8nLCT4cAPK59Qp9Ek
5+
-----END PRIVATE KEY-----

tests/keys/testkey_ec.pub

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
-----BEGIN PUBLIC KEY-----
2-
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBWcJwPEAnS/k4kFgUhxNF7J0SQQhZ
3-
G+nNgy+/mXwhQ5PZIUmId1a1TjkNXiKzv6DpttBqduHbz/V0EtH+QfWy0B4BhZ5M
4-
nTyDGjcz1DQqKdexebhzobbhSIZjpYd5aU48o9rXp/OnAnrajddpGsJ0bNf4rtML
5-
BqFYJN6LOslAB7xTBRg=
2+
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHzAcUWSlGBHcuf3y3RiNrWI+pE6+
3+
dD2T7fIzg9t6wEe3YbTaRtaI6qKZh9AB+cBF2nYJPJywk+HADyufUKfRJA==
64
-----END PUBLIC KEY-----

tests/keys/testkey_ec_ssh.pub

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAFZwnA8QCdL+TiQWBSHE0XsnRJBCFkb6c2DL7+ZfCFDk9khSYh3VrVOOQ1eIrO/oOm20Gp24dvP9XQS0f5B9bLQHgGFnkydPIMaNzPUNCop17F5uHOhtuFIhmOlh3lpTjyj2ten86cCetqN12kawnRs1/iu0wsGoVgk3os6yUAHvFMFGA==
1+
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBB8wHFFkpRgR3Ln98t0Yja1iPqROvnQ9k+3yM4PbesBHt2G02kbWiOqimYfQAfnARdp2CTycsJPhwA8rn1Cn0SQ=

tests/test_api_jws.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -224,12 +224,9 @@ def test_decodes_valid_es384_jws(self, jws):
224224
with open("tests/keys/testkey_ec.pub", "r") as fp:
225225
example_pubkey = fp.read()
226226
example_jws = (
227-
b"eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9"
228-
b".eyJoZWxsbyI6IndvcmxkIn0"
229-
b".AGtlemKghaIaYh1yeeekFH9fRuNY7hCaw5hUgZ5aG1N"
230-
b"2F8FIbiKLaZKr8SiFdTimXFVTEmxpBQ9sRmdsDsnrM-1"
231-
b"HAG0_zxxu0JyINOFT2iqF3URYl9HZ8kZWMeZAtXmn6Cw"
232-
b"PXRJD2f7N-f7bJ5JeL9VT5beI2XD3FlK3GgRvI-eE-2Ik"
227+
b"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9."
228+
b"eyJoZWxsbyI6IndvcmxkIn0.TORyNQab_MoXM7DvNKaTwbrJr4UY"
229+
b"d2SsX8hhlnWelQFmPFSf_JzC2EbLnar92t-bXsDovzxp25ExazrVHkfPkQ"
233230
)
234231
decoded_payload = jws.decode(example_jws, example_pubkey)
235232
json_payload = json.loads(force_unicode(decoded_payload))

tests/test_api_jwt.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -199,20 +199,17 @@ def test_encode_datetime(self, jwt):
199199
@pytest.mark.skipif(
200200
not has_crypto, reason="Can't run without cryptography library"
201201
)
202-
def test_decodes_valid_es384_jwt(self, jwt):
202+
def test_decodes_valid_es256_jwt(self, jwt):
203203
example_payload = {"hello": "world"}
204204
with open("tests/keys/testkey_ec.pub", "r") as fp:
205205
example_pubkey = fp.read()
206206
example_jwt = (
207-
b"eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9"
208-
b".eyJoZWxsbyI6IndvcmxkIn0"
209-
b".AddMgkmRhzqptDYqlmy_f2dzM6O9YZmVo-txs_CeAJD"
210-
b"NoD8LN7YiPeLmtIhkO5_VZeHHKvtQcGc4lsq-Y72c4dK"
211-
b"pANr1f6HEYhjpBc03u_bv06PYMcr5N2-9k97-qf-JCSb"
212-
b"zqW6R250Q7gNCX5R7NrCl7MTM4DTBZkGbUlqsFUleiGlj"
207+
b"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9."
208+
b"eyJoZWxsbyI6IndvcmxkIn0.TORyNQab_MoXM7DvNKaTwbrJr4UY"
209+
b"d2SsX8hhlnWelQFmPFSf_JzC2EbLnar92t-bXsDovzxp25ExazrVHkfPkQ"
213210
)
214-
decoded_payload = jwt.decode(example_jwt, example_pubkey)
215211

212+
decoded_payload = jwt.decode(example_jwt, example_pubkey)
216213
assert decoded_payload == example_payload
217214

218215
# 'Control' RSA JWT created by another library.

0 commit comments

Comments
 (0)