Skip to content

Commit 507d2c9

Browse files
committed
Revived PyCrypto and ecdsa-based algorithms as optional jwt.contrib modules.
1 parent d9727ca commit 507d2c9

File tree

9 files changed

+275
-12
lines changed

9 files changed

+275
-12
lines changed

jwt/contrib/__init__.py

Whitespace-only changes.

jwt/contrib/algorithms/__init__.py

Whitespace-only changes.

jwt/contrib/algorithms/py_ecdsa.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Note: This file is named py_ecdsa.py because import behavior in Python 2
2+
# would cause ecdsa.py to squash the ecdsa library that it depends upon.
3+
4+
import hashlib
5+
6+
import ecdsa
7+
8+
from jwt.algorithms import Algorithm
9+
from jwt.compat import string_types, text_type
10+
11+
12+
class ECAlgorithm(Algorithm):
13+
"""
14+
Performs signing and verification operations using
15+
ECDSA and the specified hash function
16+
17+
This class requires the ecdsa package to be installed.
18+
19+
This is based off of the implementation in PyJWT 0.3.2
20+
"""
21+
def __init__(self, hash_alg):
22+
self.hash_alg = hash_alg
23+
24+
SHA256, SHA384, SHA512 = hashlib.sha256, hashlib.sha384, hashlib.sha512
25+
26+
def prepare_key(self, key):
27+
28+
if isinstance(key, ecdsa.SigningKey) or \
29+
isinstance(key, ecdsa.VerifyingKey):
30+
return key
31+
32+
if isinstance(key, string_types):
33+
if isinstance(key, text_type):
34+
key = key.encode('utf-8')
35+
36+
# Attempt to load key. We don't know if it's
37+
# a Signing Key or a Verifying Key, so we try
38+
# the Verifying Key first.
39+
try:
40+
key = ecdsa.VerifyingKey.from_pem(key)
41+
except ecdsa.der.UnexpectedDER:
42+
key = ecdsa.SigningKey.from_pem(key)
43+
44+
else:
45+
raise TypeError('Expecting a PEM-formatted key.')
46+
47+
return key
48+
49+
def sign(self, msg, key):
50+
return key.sign(msg, hashfunc=self.hash_alg,
51+
sigencode=ecdsa.util.sigencode_der)
52+
53+
def verify(self, msg, key, sig):
54+
try:
55+
return key.verify(sig, msg, hashfunc=self.hash_alg,
56+
sigdecode=ecdsa.util.sigdecode_der)
57+
except ecdsa.der.UnexpectedDER:
58+
return False

jwt/contrib/algorithms/pycrypto.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import Crypto.Hash.SHA256
2+
import Crypto.Hash.SHA384
3+
import Crypto.Hash.SHA512
4+
5+
from Crypto.PublicKey import RSA
6+
from Crypto.Signature import PKCS1_v1_5
7+
8+
from jwt.algorithms import Algorithm
9+
from jwt.compat import string_types, text_type
10+
11+
12+
class RSAAlgorithm(Algorithm):
13+
"""
14+
Performs signing and verification operations using
15+
RSASSA-PKCS-v1_5 and the specified hash function.
16+
17+
This class requires PyCrypto package to be installed.
18+
19+
This is based off of the implementation in PyJWT 0.3.2
20+
"""
21+
def __init__(self, hash_alg):
22+
self.hash_alg = hash_alg
23+
24+
SHA256, SHA384, SHA512 = (Crypto.Hash.SHA256, Crypto.Hash.SHA384,
25+
Crypto.Hash.SHA512)
26+
27+
def prepare_key(self, key):
28+
29+
if isinstance(key, RSA._RSAobj):
30+
return key
31+
32+
if isinstance(key, string_types):
33+
if isinstance(key, text_type):
34+
key = key.encode('utf-8')
35+
36+
key = RSA.importKey(key)
37+
else:
38+
raise TypeError('Expecting a PEM- or RSA-formatted key.')
39+
40+
return key
41+
42+
def sign(self, msg, key):
43+
return PKCS1_v1_5.new(key).sign(self.hash_alg.new(msg))
44+
45+
def verify(self, msg, key, sig):
46+
return PKCS1_v1_5.new(key).verify(self.hash_alg.new(msg), sig)

tests/contrib/__init__.py

Whitespace-only changes.

tests/contrib/test_algorithms.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import base64
2+
3+
from ..compat import unittest
4+
from ..utils import ensure_bytes, ensure_unicode, key_path
5+
6+
try:
7+
from jwt.contrib.algorithms.pycrypto import RSAAlgorithm
8+
has_pycrypto = True
9+
except ImportError:
10+
has_pycrypto = False
11+
12+
try:
13+
from jwt.contrib.algorithms.py_ecdsa import ECAlgorithm
14+
has_ecdsa = True
15+
except ImportError:
16+
has_ecdsa = False
17+
18+
19+
@unittest.skipIf(not has_pycrypto, 'Not supported without PyCrypto library')
20+
class TestPycryptoAlgorithms(unittest.TestCase):
21+
def setUp(self): # noqa
22+
pass
23+
24+
def test_rsa_should_parse_pem_public_key(self):
25+
algo = RSAAlgorithm(RSAAlgorithm.SHA256)
26+
27+
with open(key_path('testkey2_rsa.pub.pem'), 'r') as pem_key:
28+
algo.prepare_key(pem_key.read())
29+
30+
def test_rsa_should_accept_unicode_key(self):
31+
algo = RSAAlgorithm(RSAAlgorithm.SHA256)
32+
33+
with open(key_path('testkey_rsa'), 'r') as rsa_key:
34+
algo.prepare_key(ensure_unicode(rsa_key.read()))
35+
36+
def test_rsa_should_reject_non_string_key(self):
37+
algo = RSAAlgorithm(RSAAlgorithm.SHA256)
38+
39+
with self.assertRaises(TypeError):
40+
algo.prepare_key(None)
41+
42+
def test_rsa_verify_should_return_false_if_signature_invalid(self):
43+
algo = RSAAlgorithm(RSAAlgorithm.SHA256)
44+
45+
jwt_message = ensure_bytes('Hello World!')
46+
47+
jwt_sig = base64.b64decode(ensure_bytes(
48+
'yS6zk9DBkuGTtcBzLUzSpo9gGJxJFOGvUqN01iLhWHrzBQ9ZEz3+Ae38AXp'
49+
'10RWwscp42ySC85Z6zoN67yGkLNWnfmCZSEv+xqELGEvBJvciOKsrhiObUl'
50+
'2mveSc1oeO/2ujkGDkkkJ2epn0YliacVjZF5+/uDmImUfAAj8lzjnHlzYix'
51+
'sn5jGz1H07jYYbi9diixN8IUhXeTafwFg02IcONhum29V40Wu6O5tAKWlJX'
52+
'fHJnNUzAEUOXS0WahHVb57D30pcgIji9z923q90p5c7E2cU8V+E1qe8NdCA'
53+
'APCDzZZ9zQ/dgcMVaBrGrgimrcLbPjueOKFgSO+SSjIElKA=='))
54+
55+
jwt_sig += ensure_bytes('123') # Signature is now invalid
56+
57+
with open(key_path('testkey_rsa.pub'), 'r') as keyfile:
58+
jwt_pub_key = algo.prepare_key(keyfile.read())
59+
60+
result = algo.verify(jwt_message, jwt_pub_key, jwt_sig)
61+
self.assertFalse(result)
62+
63+
def test_rsa_verify_should_return_true_if_signature_valid(self):
64+
algo = RSAAlgorithm(RSAAlgorithm.SHA256)
65+
66+
jwt_message = ensure_bytes('Hello World!')
67+
68+
jwt_sig = base64.b64decode(ensure_bytes(
69+
'yS6zk9DBkuGTtcBzLUzSpo9gGJxJFOGvUqN01iLhWHrzBQ9ZEz3+Ae38AXp'
70+
'10RWwscp42ySC85Z6zoN67yGkLNWnfmCZSEv+xqELGEvBJvciOKsrhiObUl'
71+
'2mveSc1oeO/2ujkGDkkkJ2epn0YliacVjZF5+/uDmImUfAAj8lzjnHlzYix'
72+
'sn5jGz1H07jYYbi9diixN8IUhXeTafwFg02IcONhum29V40Wu6O5tAKWlJX'
73+
'fHJnNUzAEUOXS0WahHVb57D30pcgIji9z923q90p5c7E2cU8V+E1qe8NdCA'
74+
'APCDzZZ9zQ/dgcMVaBrGrgimrcLbPjueOKFgSO+SSjIElKA=='))
75+
76+
with open(key_path('testkey_rsa.pub'), 'r') as keyfile:
77+
jwt_pub_key = algo.prepare_key(keyfile.read())
78+
79+
result = algo.verify(jwt_message, jwt_pub_key, jwt_sig)
80+
self.assertTrue(result)
81+
82+
83+
@unittest.skipIf(not has_ecdsa, 'Not supported without ecdsa library')
84+
class TestEcdsaAlgorithms(unittest.TestCase):
85+
def test_ec_should_reject_non_string_key(self):
86+
algo = ECAlgorithm(ECAlgorithm.SHA256)
87+
88+
with self.assertRaises(TypeError):
89+
algo.prepare_key(None)
90+
91+
def test_ec_should_accept_unicode_key(self):
92+
algo = ECAlgorithm(ECAlgorithm.SHA256)
93+
94+
with open(key_path('testkey_ec'), 'r') as ec_key:
95+
algo.prepare_key(ensure_unicode(ec_key.read()))
96+
97+
def test_ec_verify_should_return_false_if_signature_invalid(self):
98+
algo = ECAlgorithm(ECAlgorithm.SHA256)
99+
100+
jwt_message = ensure_bytes('Hello World!')
101+
102+
jwt_sig = base64.b64decode(ensure_bytes(
103+
'MIGIAkIB9vYz+inBL8aOTA4auYz/zVuig7TT1bQgKROIQX9YpViHkFa4DT5'
104+
'5FuFKn9XzVlk90p6ldEj42DC9YecXHbC2t+cCQgCicY+8f3f/KCNtWK7cif'
105+
'6vdsVwm6Lrjs0Ag6ZqCf+olN11hVt1qKBC4lXppqB1gNWEmNQaiz1z2QRyc'
106+
'zJ8hSJmbw=='))
107+
108+
jwt_sig += ensure_bytes('123') # Signature is now invalid
109+
110+
with open(key_path('testkey_ec.pub'), 'r') as keyfile:
111+
jwt_pub_key = algo.prepare_key(keyfile.read())
112+
113+
result = algo.verify(jwt_message, jwt_pub_key, jwt_sig)
114+
self.assertFalse(result)
115+
116+
def test_ec_verify_should_return_true_if_signature_valid(self):
117+
algo = ECAlgorithm(ECAlgorithm.SHA256)
118+
119+
jwt_message = ensure_bytes('Hello World!')
120+
121+
jwt_sig = base64.b64decode(ensure_bytes(
122+
'MIGIAkIB9vYz+inBL8aOTA4auYz/zVuig7TT1bQgKROIQX9YpViHkFa4DT5'
123+
'5FuFKn9XzVlk90p6ldEj42DC9YecXHbC2t+cCQgCicY+8f3f/KCNtWK7cif'
124+
'6vdsVwm6Lrjs0Ag6ZqCf+olN11hVt1qKBC4lXppqB1gNWEmNQaiz1z2QRyc'
125+
'zJ8hSJmbw=='))
126+
127+
with open(key_path('testkey_ec.pub'), 'r') as keyfile:
128+
jwt_pub_key = algo.prepare_key(keyfile.read())
129+
130+
result = algo.verify(jwt_message, jwt_pub_key, jwt_sig)
131+
self.assertTrue(result)

tests/test_algorithms.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from jwt.algorithms import Algorithm, HMACAlgorithm
44

55
from .compat import unittest
6-
from .utils import ensure_bytes, ensure_unicode
6+
from .utils import ensure_bytes, ensure_unicode, key_path
77

88
try:
99
from jwt.algorithms import RSAAlgorithm, ECAlgorithm
@@ -53,14 +53,14 @@ def test_hmac_should_accept_unicode_key(self):
5353
def test_rsa_should_parse_pem_public_key(self):
5454
algo = RSAAlgorithm(RSAAlgorithm.SHA256)
5555

56-
with open('tests/keys/testkey2_rsa.pub.pem', 'r') as pem_key:
56+
with open(key_path('testkey2_rsa.pub.pem'), 'r') as pem_key:
5757
algo.prepare_key(pem_key.read())
5858

5959
@unittest.skipIf(not has_crypto, 'Not supported without cryptography library')
6060
def test_rsa_should_accept_unicode_key(self):
6161
algo = RSAAlgorithm(RSAAlgorithm.SHA256)
6262

63-
with open('tests/keys/testkey_rsa', 'r') as rsa_key:
63+
with open(key_path('testkey_rsa'), 'r') as rsa_key:
6464
algo.prepare_key(ensure_unicode(rsa_key.read()))
6565

6666
@unittest.skipIf(not has_crypto, 'Not supported without cryptography library')
@@ -84,9 +84,9 @@ def test_rsa_verify_should_return_false_if_signature_invalid(self):
8484
'fHJnNUzAEUOXS0WahHVb57D30pcgIji9z923q90p5c7E2cU8V+E1qe8NdCA'
8585
'APCDzZZ9zQ/dgcMVaBrGrgimrcLbPjueOKFgSO+SSjIElKA=='))
8686

87-
jwt_sig = jwt_sig + ensure_bytes('123') # Signature is now invalid
87+
jwt_sig += ensure_bytes('123') # Signature is now invalid
8888

89-
with open('tests/keys/testkey_rsa.pub', 'r') as keyfile:
89+
with open(key_path('testkey_rsa.pub'), 'r') as keyfile:
9090
jwt_pub_key = algo.prepare_key(keyfile.read())
9191

9292
result = algo.verify(jwt_message, jwt_pub_key, jwt_sig)
@@ -106,7 +106,7 @@ def test_rsa_verify_should_return_true_if_signature_valid(self):
106106
'fHJnNUzAEUOXS0WahHVb57D30pcgIji9z923q90p5c7E2cU8V+E1qe8NdCA'
107107
'APCDzZZ9zQ/dgcMVaBrGrgimrcLbPjueOKFgSO+SSjIElKA=='))
108108

109-
with open('tests/keys/testkey_rsa.pub', 'r') as keyfile:
109+
with open(key_path('testkey_rsa.pub'), 'r') as keyfile:
110110
jwt_pub_key = algo.prepare_key(keyfile.read())
111111

112112
result = algo.verify(jwt_message, jwt_pub_key, jwt_sig)
@@ -123,7 +123,7 @@ def test_ec_should_reject_non_string_key(self):
123123
def test_ec_should_accept_unicode_key(self):
124124
algo = ECAlgorithm(ECAlgorithm.SHA256)
125125

126-
with open('tests/keys/testkey_ec', 'r') as ec_key:
126+
with open(key_path('testkey_ec'), 'r') as ec_key:
127127
algo.prepare_key(ensure_unicode(ec_key.read()))
128128

129129
@unittest.skipIf(not has_crypto, 'Not supported without cryptography library')
@@ -138,9 +138,9 @@ def test_ec_verify_should_return_false_if_signature_invalid(self):
138138
'6vdsVwm6Lrjs0Ag6ZqCf+olN11hVt1qKBC4lXppqB1gNWEmNQaiz1z2QRyc'
139139
'zJ8hSJmbw=='))
140140

141-
jwt_sig = ensure_bytes('123') # Signature is now invalid
141+
jwt_sig += ensure_bytes('123') # Signature is now invalid
142142

143-
with open('tests/keys/testkey_ec.pub', 'r') as keyfile:
143+
with open(key_path('testkey_ec.pub'), 'r') as keyfile:
144144
jwt_pub_key = algo.prepare_key(keyfile.read())
145145

146146
result = algo.verify(jwt_message, jwt_pub_key, jwt_sig)
@@ -158,7 +158,7 @@ def test_ec_verify_should_return_true_if_signature_valid(self):
158158
'6vdsVwm6Lrjs0Ag6ZqCf+olN11hVt1qKBC4lXppqB1gNWEmNQaiz1z2QRyc'
159159
'zJ8hSJmbw=='))
160160

161-
with open('tests/keys/testkey_ec.pub', 'r') as keyfile:
161+
with open(key_path('testkey_ec.pub'), 'r') as keyfile:
162162
jwt_pub_key = algo.prepare_key(keyfile.read())
163163

164164
result = algo.verify(jwt_message, jwt_pub_key, jwt_sig)

tests/utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import os
2+
13
from .compat import text_type
24

35

@@ -13,3 +15,8 @@ def ensure_unicode(key):
1315
key = key.decode()
1416

1517
return key
18+
19+
20+
def key_path(key_name):
21+
return os.path.join(os.path.dirname(os.path.realpath(__file__)),
22+
'keys', key_name)

tox.ini

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tox]
2-
envlist = py26, py27, py32, py33, py34, py34-nocrypto, pep8
2+
envlist = py26, py27, py27-contrib-crypto, py27-nocrypto, py32, py33, py34, py34-contrib-crypto, py34-nocrypto, pep8
33

44
[testenv]
55
commands =
@@ -11,6 +11,28 @@ deps =
1111
unittest2
1212
coverage
1313

14+
[testenv:py34-contrib-crypto]
15+
basepython = python3.4
16+
commands =
17+
coverage erase
18+
coverage run setup.py test
19+
coverage report -m
20+
deps =
21+
pycrypto
22+
ecdsa
23+
coverage
24+
25+
[testenv:py27-contrib-crypto]
26+
basepython = python2.7
27+
commands =
28+
coverage erase
29+
coverage run setup.py test
30+
coverage report -m
31+
deps =
32+
pycrypto
33+
ecdsa
34+
coverage
35+
1436
[testenv:py34-nocrypto]
1537
basepython = python3.4
1638
commands =
@@ -35,6 +57,5 @@ deps =
3557
flake8
3658
flake8-import-order
3759
pep8-naming
38-
unittest2
3960
commands =
4061
flake8 . --max-line-length=120

0 commit comments

Comments
 (0)