Skip to content

Commit 069e684

Browse files
authored
Merge pull request #49 from zejn/master
Implement cryptography as one of the backends
2 parents 1a23c39 + 7d83918 commit 069e684

File tree

14 files changed

+753
-246
lines changed

14 files changed

+753
-246
lines changed

.travis.yml

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1+
sudo: false
12
language: python
2-
env:
3-
- TOXENV=py26
4-
- TOXENV=py27
5-
- TOXENV=py33
6-
- TOXENV=py34
7-
- TOXENV=pypy
8-
install:
9-
- pip install -r requirements-dev.txt
10-
- pip install -U tox codecov
11-
script:
3+
python:
4+
- "2.6"
5+
- "2.7"
6+
- "3.3"
7+
- "3.4"
8+
- "3.5"
9+
- "3.6"
10+
- "pypy-5.3.1"
11+
install:
12+
- pip install -U tox codecov tox-travis
13+
script:
1214
- tox
1315
after_success:
1416
- codecov

jose/backends/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
try:
3+
from jose.backends.pycrypto_backend import RSAKey
4+
except ImportError:
5+
from jose.backends.cryptography_backend import CryptographyRSAKey as RSAKey
6+
7+
try:
8+
from jose.backends.cryptography_backend import CryptographyECKey as ECKey
9+
except ImportError:
10+
from jose.backends.ecdsa_backend import ECDSAECKey as ECKey

jose/backends/base.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
class Key(object):
2+
"""
3+
A simple interface for implementing JWK keys.
4+
"""
5+
def __init__(self, key, algorithm):
6+
pass
7+
8+
def sign(self, msg):
9+
raise NotImplementedError()
10+
11+
def verify(self, msg, sig):
12+
raise NotImplementedError()
13+
14+
def public_key(self):
15+
raise NotImplementedError()
16+
17+
def to_pem(self):
18+
raise NotImplementedError()
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import six
2+
import ecdsa
3+
from ecdsa.util import sigdecode_string, sigencode_string, sigdecode_der, sigencode_der
4+
5+
from jose.backends.base import Key
6+
from jose.utils import base64_to_long
7+
from jose.constants import ALGORITHMS
8+
from jose.exceptions import JWKError
9+
10+
from cryptography.exceptions import InvalidSignature
11+
from cryptography.hazmat.backends import default_backend
12+
from cryptography.hazmat.primitives import hashes, serialization
13+
from cryptography.hazmat.primitives.asymmetric import ec, rsa, padding
14+
from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key
15+
16+
17+
class CryptographyECKey(Key):
18+
SHA256 = hashes.SHA256
19+
SHA384 = hashes.SHA384
20+
SHA512 = hashes.SHA512
21+
22+
def __init__(self, key, algorithm, cryptography_backend=default_backend):
23+
if algorithm not in ALGORITHMS.EC:
24+
raise JWKError('hash_alg: %s is not a valid hash algorithm' % algorithm)
25+
26+
self.hash_alg = {
27+
ALGORITHMS.ES256: self.SHA256,
28+
ALGORITHMS.ES384: self.SHA384,
29+
ALGORITHMS.ES512: self.SHA512
30+
}.get(algorithm)
31+
self._algorithm = algorithm
32+
33+
self.cryptography_backend = cryptography_backend
34+
35+
if hasattr(key, 'public_bytes') or hasattr(key, 'private_bytes'):
36+
self.prepared_key = key
37+
return
38+
39+
if isinstance(key, (ecdsa.SigningKey, ecdsa.VerifyingKey)):
40+
# convert to PEM and let cryptography below load it as PEM
41+
key = key.to_pem().decode('utf-8')
42+
43+
if isinstance(key, dict):
44+
self.prepared_key = self._process_jwk(key)
45+
return
46+
47+
if isinstance(key, six.string_types):
48+
key = key.encode('utf-8')
49+
50+
if isinstance(key, six.binary_type):
51+
# Attempt to load key. We don't know if it's
52+
# a Public Key or a Private Key, so we try
53+
# the Public Key first.
54+
try:
55+
try:
56+
key = load_pem_public_key(key, self.cryptography_backend())
57+
except ValueError:
58+
key = load_pem_private_key(key, password=None, backend=self.cryptography_backend())
59+
except Exception as e:
60+
raise JWKError(e)
61+
62+
self.prepared_key = key
63+
return
64+
65+
raise JWKError('Unable to parse an ECKey from key: %s' % key)
66+
67+
def _process_jwk(self, jwk_dict):
68+
if not jwk_dict.get('kty') == 'EC':
69+
raise JWKError("Incorrect key type. Expected: 'EC', Recieved: %s" % jwk_dict.get('kty'))
70+
71+
x = base64_to_long(jwk_dict.get('x'))
72+
y = base64_to_long(jwk_dict.get('y'))
73+
74+
curve = {
75+
'P-256': ec.SECP256R1,
76+
'P-384': ec.SECP384R1,
77+
'P-521': ec.SECP521R1,
78+
}[jwk_dict['crv']]
79+
80+
ec_pn = ec.EllipticCurvePublicNumbers(x, y, curve())
81+
verifying_key = ec_pn.public_key(self.cryptography_backend())
82+
83+
return verifying_key
84+
85+
def sign(self, msg):
86+
if self.hash_alg.digest_size * 8 > self.prepared_key.curve.key_size:
87+
raise TypeError("this curve (%s) is too short "
88+
"for your digest (%d)" % (self.prepared_key.curve.name,
89+
8*self.hash_alg.digest_size))
90+
signature = self.prepared_key.sign(msg, ec.ECDSA(self.hash_alg()))
91+
order = (2 ** self.prepared_key.curve.key_size) - 1
92+
return sigencode_string(*sigdecode_der(signature, order), order=order)
93+
94+
def verify(self, msg, sig):
95+
order = (2 ** self.prepared_key.curve.key_size) - 1
96+
signature = sigencode_der(*sigdecode_string(sig, order), order=order)
97+
verifier = self.prepared_key.verifier(signature, ec.ECDSA(self.hash_alg()))
98+
verifier.update(msg)
99+
try:
100+
return verifier.verify()
101+
except:
102+
return False
103+
104+
def public_key(self):
105+
if hasattr(self.prepared_key, 'public_bytes'):
106+
return self
107+
return self.__class__(self.prepared_key.public_key(), self._algorithm)
108+
109+
def to_pem(self):
110+
if hasattr(self.prepared_key, 'public_bytes'):
111+
pem = self.prepared_key.public_bytes(
112+
encoding=serialization.Encoding.PEM,
113+
format=serialization.PublicFormat.SubjectPublicKeyInfo
114+
)
115+
return pem
116+
pem = self.prepared_key.private_bytes(
117+
encoding=serialization.Encoding.PEM,
118+
format=serialization.PrivateFormat.TraditionalOpenSSL,
119+
encryption_algorithm=serialization.NoEncryption()
120+
)
121+
return pem
122+
123+
124+
class CryptographyRSAKey(Key):
125+
SHA256 = hashes.SHA256
126+
SHA384 = hashes.SHA384
127+
SHA512 = hashes.SHA512
128+
129+
def __init__(self, key, algorithm, cryptography_backend=default_backend):
130+
if algorithm not in ALGORITHMS.RSA:
131+
raise JWKError('hash_alg: %s is not a valid hash algorithm' % algorithm)
132+
133+
self.hash_alg = {
134+
ALGORITHMS.RS256: self.SHA256,
135+
ALGORITHMS.RS384: self.SHA384,
136+
ALGORITHMS.RS512: self.SHA512
137+
}.get(algorithm)
138+
self._algorithm = algorithm
139+
140+
self.cryptography_backend = cryptography_backend
141+
142+
# if it conforms to RSAPublicKey interface
143+
if hasattr(key, 'public_bytes') and hasattr(key, 'public_numbers'):
144+
self.prepared_key = key
145+
return
146+
147+
if isinstance(key, dict):
148+
self.prepared_key = self._process_jwk(key)
149+
return
150+
151+
if isinstance(key, six.string_types):
152+
key = key.encode('utf-8')
153+
154+
if isinstance(key, six.binary_type):
155+
try:
156+
try:
157+
key = load_pem_public_key(key, self.cryptography_backend())
158+
except ValueError:
159+
key = load_pem_private_key(key, password=None, backend=self.cryptography_backend())
160+
self.prepared_key = key
161+
except Exception as e:
162+
raise JWKError(e)
163+
return
164+
165+
raise JWKError('Unable to parse an RSA_JWK from key: %s' % key)
166+
167+
def _process_jwk(self, jwk_dict):
168+
if not jwk_dict.get('kty') == 'RSA':
169+
raise JWKError("Incorrect key type. Expected: 'RSA', Recieved: %s" % jwk_dict.get('kty'))
170+
171+
e = base64_to_long(jwk_dict.get('e', 256))
172+
n = base64_to_long(jwk_dict.get('n'))
173+
174+
verifying_key = rsa.RSAPublicNumbers(e, n).public_key(self.cryptography_backend())
175+
return verifying_key
176+
177+
def sign(self, msg):
178+
signer = self.prepared_key.signer(
179+
padding.PKCS1v15(),
180+
self.hash_alg()
181+
)
182+
signer.update(msg)
183+
signature = signer.finalize()
184+
return signature
185+
186+
def verify(self, msg, sig):
187+
verifier = self.prepared_key.verifier(
188+
sig,
189+
padding.PKCS1v15(),
190+
self.hash_alg()
191+
)
192+
verifier.update(msg)
193+
try:
194+
verifier.verify()
195+
return True
196+
except InvalidSignature:
197+
return False
198+
199+
def public_key(self):
200+
if hasattr(self.prepared_key, 'public_bytes'):
201+
return self
202+
return self.__class__(self.prepared_key.public_key(), self._algorithm)
203+
204+
def to_pem(self):
205+
if hasattr(self.prepared_key, 'public_bytes'):
206+
return self.prepared_key.public_bytes(
207+
encoding=serialization.Encoding.PEM,
208+
format=serialization.PublicFormat.SubjectPublicKeyInfo
209+
)
210+
211+
return self.prepared_key.private_bytes(
212+
encoding=serialization.Encoding.PEM,
213+
format=serialization.PrivateFormat.TraditionalOpenSSL,
214+
encryption_algorithm=serialization.NoEncryption()
215+
)

jose/backends/ecdsa_backend.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import hashlib
2+
import six
3+
4+
from jose.backends.base import Key
5+
import ecdsa
6+
7+
from jose.constants import ALGORITHMS
8+
from jose.exceptions import JWKError
9+
from jose.utils import base64_to_long
10+
11+
12+
class ECDSAECKey(Key):
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+
SHA256 = hashlib.sha256
22+
SHA384 = hashlib.sha384
23+
SHA512 = hashlib.sha512
24+
25+
CURVE_MAP = {
26+
SHA256: ecdsa.curves.NIST256p,
27+
SHA384: ecdsa.curves.NIST384p,
28+
SHA512: ecdsa.curves.NIST521p,
29+
}
30+
31+
def __init__(self, key, algorithm):
32+
if algorithm not in ALGORITHMS.EC:
33+
raise JWKError('hash_alg: %s is not a valid hash algorithm' % algorithm)
34+
35+
self.hash_alg = {
36+
ALGORITHMS.ES256: self.SHA256,
37+
ALGORITHMS.ES384: self.SHA384,
38+
ALGORITHMS.ES512: self.SHA512
39+
}.get(algorithm)
40+
self._algorithm = algorithm
41+
42+
self.curve = self.CURVE_MAP.get(self.hash_alg)
43+
44+
if isinstance(key, (ecdsa.SigningKey, ecdsa.VerifyingKey)):
45+
self.prepared_key = key
46+
return
47+
48+
if isinstance(key, dict):
49+
self.prepared_key = self._process_jwk(key)
50+
return
51+
52+
if isinstance(key, six.string_types):
53+
key = key.encode('utf-8')
54+
55+
if isinstance(key, six.binary_type):
56+
# Attempt to load key. We don't know if it's
57+
# a Signing Key or a Verifying Key, so we try
58+
# the Verifying Key first.
59+
try:
60+
key = ecdsa.VerifyingKey.from_pem(key)
61+
except ecdsa.der.UnexpectedDER:
62+
key = ecdsa.SigningKey.from_pem(key)
63+
except Exception as e:
64+
raise JWKError(e)
65+
66+
self.prepared_key = key
67+
return
68+
69+
raise JWKError('Unable to parse an ECKey from key: %s' % key)
70+
71+
def _process_jwk(self, jwk_dict):
72+
if not jwk_dict.get('kty') == 'EC':
73+
raise JWKError("Incorrect key type. Expected: 'EC', Recieved: %s" % jwk_dict.get('kty'))
74+
75+
x = base64_to_long(jwk_dict.get('x'))
76+
y = base64_to_long(jwk_dict.get('y'))
77+
78+
if not ecdsa.ecdsa.point_is_valid(self.curve.generator, x, y):
79+
raise JWKError("Point: %s, %s is not a valid point" % (x, y))
80+
81+
point = ecdsa.ellipticcurve.Point(self.curve.curve, x, y, self.curve.order)
82+
verifying_key = ecdsa.keys.VerifyingKey.from_public_point(point, self.curve)
83+
84+
return verifying_key
85+
86+
def sign(self, msg):
87+
return self.prepared_key.sign(msg, hashfunc=self.hash_alg, sigencode=ecdsa.util.sigencode_string)
88+
89+
def verify(self, msg, sig):
90+
try:
91+
return self.prepared_key.verify(sig, msg, hashfunc=self.hash_alg, sigdecode=ecdsa.util.sigdecode_string)
92+
except:
93+
return False
94+
95+
def public_key(self):
96+
if isinstance(self.prepared_key, ecdsa.VerifyingKey):
97+
return self
98+
return self.__class__(self.prepared_key.get_verifying_key(), self._algorithm)
99+
100+
def to_pem(self):
101+
return self.prepared_key.to_pem()

0 commit comments

Comments
 (0)