Skip to content

Commit fbd122d

Browse files
author
Gasper Zejn
committed
Refactor Key into jose.backends.base to make it importable into backends. Use pycrypto backend by default for RSA. Add to_pem and public_key to EC and RSA keys in order to abstract away backend key implementations and be able to convert between keys. Move base64_to_long and int_arr_to_long to jose.utils to be importable by backends.
1 parent b611473 commit fbd122d

File tree

8 files changed

+126
-48
lines changed

8 files changed

+126
-48
lines changed

jose/backends/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
try:
3+
from .pycrypto_backend import RSAKey
4+
except ImportError:
5+
from .cryptography_backend import CryptographyRSAKey as RSAKey

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()

jose/backends/cryptography_backend.py

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
import ecdsa
33
from ecdsa.util import sigdecode_string, sigencode_string, sigdecode_der, sigencode_der
44

5-
from jose.jwk import Key, base64_to_long
5+
from jose.backends.base import Key
6+
from jose.utils import base64_to_long
67
from jose.constants import ALGORITHMS
78
from jose.exceptions import JWKError
89

910
from cryptography.exceptions import InvalidSignature
1011
from cryptography.hazmat.backends import default_backend
11-
from cryptography.hazmat.backends.openssl.rsa import _RSAPublicKey
12-
from cryptography.hazmat.primitives import hashes
12+
from cryptography.hazmat.primitives import hashes, serialization
1313
from cryptography.hazmat.primitives.asymmetric import ec, rsa, padding
1414
from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key
1515

@@ -34,10 +34,15 @@ def __init__(self, key, algorithm, cryptography_backend=default_backend):
3434
ALGORITHMS.ES384: self.SHA384,
3535
ALGORITHMS.ES512: self.SHA512
3636
}.get(algorithm)
37+
self._algorithm = algorithm
3738

3839
self.curve = self.CURVE_MAP.get(self.hash_alg)
3940
self.cryptography_backend = cryptography_backend
4041

42+
if hasattr(key, 'public_bytes') or hasattr(key, 'private_bytes'):
43+
self.prepared_key = key
44+
return
45+
4146
if isinstance(key, (ecdsa.SigningKey, ecdsa.VerifyingKey)):
4247
# convert to PEM and let cryptography below load it as PEM
4348
key = key.to_pem().decode('utf-8')
@@ -93,6 +98,25 @@ def verify(self, msg, sig):
9398
except:
9499
return False
95100

101+
def public_key(self):
102+
if hasattr(self.prepared_key, 'public_bytes'):
103+
return self
104+
return self.__class__(self.prepared_key.public_key(), self._algorithm)
105+
106+
def to_pem(self):
107+
if hasattr(self.prepared_key, 'public_bytes'):
108+
pem = self.prepared_key.public_bytes(
109+
encoding=serialization.Encoding.PEM,
110+
format=serialization.PublicFormat.SubjectPublicKeyInfo
111+
)
112+
return pem.decode('utf-8')
113+
pem = self.prepared_key.private_bytes(
114+
encoding=serialization.Encoding.PEM,
115+
format=serialization.PrivateFormat.TraditionalOpenSSL,
116+
encryption_algorithm=serialization.NoEncryption()
117+
)
118+
return pem.decode('utf-8')
119+
96120

97121
class CryptographyRSAKey(Key):
98122
SHA256 = hashes.SHA256
@@ -108,10 +132,12 @@ def __init__(self, key, algorithm, cryptography_backend=default_backend):
108132
ALGORITHMS.RS384: self.SHA384,
109133
ALGORITHMS.RS512: self.SHA512
110134
}.get(algorithm)
135+
self._algorithm = algorithm
111136

112137
self.cryptography_backend = cryptography_backend
113138

114-
if isinstance(key, _RSAPublicKey):
139+
# if it conforms to RSAPublicKey interface
140+
if hasattr(key, 'public_bytes') and hasattr(key, 'public_numbers'):
115141
self.prepared_key = key
116142
return
117143

@@ -166,3 +192,21 @@ def verify(self, msg, sig):
166192
return True
167193
except InvalidSignature:
168194
return False
195+
196+
def public_key(self):
197+
if hasattr(self.prepared_key, 'public_bytes'):
198+
return self
199+
return self.__class__(self.prepared_key.public_key(), self._algorithm)
200+
201+
def to_pem(self):
202+
if hasattr(self.prepared_key, 'public_bytes'):
203+
return self.prepared_key.public_bytes(
204+
encoding=serialization.Encoding.PEM,
205+
format=serialization.PublicFormat.SubjectPublicKeyInfo
206+
).decode('utf-8')
207+
208+
return self.prepared_key.private_bytes(
209+
encoding=serialization.Encoding.PEM,
210+
format=serialization.PrivateFormat.TraditionalOpenSSL,
211+
encryption_algorithm=serialization.NoEncryption()
212+
).decode('utf-8')

jose/backends/pycrypto_backend.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
from Crypto.Signature import PKCS1_v1_5
99
from Crypto.Util.asn1 import DerSequence
1010

11-
from jose.jwk import Key, base64_to_long
11+
from jose.backends.base import Key
12+
from jose.jwk import base64_to_long
1213
from jose.constants import ALGORITHMS
1314
from jose.exceptions import JWKError
1415
from jose.utils import base64url_decode
@@ -43,6 +44,7 @@ def __init__(self, key, algorithm):
4344
ALGORITHMS.RS384: self.SHA384,
4445
ALGORITHMS.RS512: self.SHA512
4546
}.get(algorithm)
47+
self._algorithm = algorithm
4648

4749
if isinstance(key, _RSAKey):
4850
self.prepared_key = key
@@ -102,3 +104,11 @@ def verify(self, msg, sig):
102104
return PKCS1_v1_5.new(self.prepared_key).verify(self.hash_alg.new(msg), sig)
103105
except Exception as e:
104106
return False
107+
108+
def public_key(self):
109+
if not self.prepared_key.key.has_private():
110+
return self
111+
return self.__class__(self.prepared_key.publickey(), self._algorithm)
112+
113+
def to_pem(self):
114+
return self.prepared_key.exportKey('PEM')

jose/jwk.py

Lines changed: 10 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,16 @@
11

2-
import base64
32
import hashlib
43
import hmac
5-
import struct
64
import six
7-
import sys
85

96
import ecdsa
107

118
from jose.constants import ALGORITHMS
129
from jose.exceptions import JWKError
13-
from jose.utils import base64url_decode
10+
from jose.utils import base64url_decode, base64_to_long
1411
from jose.utils import constant_time_string_compare
15-
16-
# Deal with integer compatibilities between Python 2 and 3.
17-
# Using `from builtins import int` is not supported on AppEngine.
18-
if sys.version_info > (3,):
19-
long = int
20-
21-
22-
def int_arr_to_long(arr):
23-
return long(''.join(["%02x" % byte for byte in arr]), 16)
24-
25-
26-
def base64_to_long(data):
27-
if isinstance(data, six.text_type):
28-
data = data.encode("ascii")
29-
30-
# urlsafe_b64decode will happily convert b64encoded data
31-
_d = base64.urlsafe_b64decode(bytes(data) + b'==')
32-
return int_arr_to_long(struct.unpack('%sB' % len(_d), _d))
12+
from jose.backends.base import Key
13+
from jose.backends import RSAKey
3314

3415

3516
def get_key(algorithm):
@@ -88,20 +69,6 @@ def get_algorithm_object(algorithm):
8869
return getattr(key, attr)
8970

9071

91-
class Key(object):
92-
"""
93-
A simple interface for implementing JWK keys.
94-
"""
95-
def __init__(self, key, algorithm):
96-
pass
97-
98-
def sign(self, msg):
99-
raise NotImplementedError()
100-
101-
def verify(self, msg, sig):
102-
raise NotImplementedError()
103-
104-
10572
class HMACKey(Key):
10673
"""
10774
Performs signing and verification operations using HMAC
@@ -180,6 +147,7 @@ def __init__(self, key, algorithm):
180147
if algorithm not in ALGORITHMS.EC:
181148
raise JWKError('hash_alg: %s is not a valid hash algorithm' % algorithm)
182149
self.hash_alg = get_algorithm_object(algorithm)
150+
self._algorithm = algorithm
183151

184152
self.curve = self.CURVE_MAP.get(self.hash_alg)
185153

@@ -234,8 +202,10 @@ def verify(self, msg, sig):
234202
except:
235203
return False
236204

205+
def public_key(self):
206+
if isinstance(self.prepared_key, ecdsa.VerifyingKey):
207+
return self
208+
return self.__class__(self.prepared_key.get_verifying_key(), self._algorithm)
237209

238-
from jose.backends.pycrypto_backend import RSAKey
239-
#ALGORITHMS.register_key('RS256', RSAKey)
240-
#ALGORITHMS.register_key('RS384', RSAKey)
241-
#ALGORITHMS.register_key('RS512', RSAKey)
210+
def to_pem(self):
211+
return self.prepared_key.to_pem()

jose/utils.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,27 @@
11

22
import base64
33
import hmac
4+
import six
5+
import struct
6+
import sys
7+
8+
# Deal with integer compatibilities between Python 2 and 3.
9+
# Using `from builtins import int` is not supported on AppEngine.
10+
if sys.version_info > (3,):
11+
long = int
12+
13+
14+
def int_arr_to_long(arr):
15+
return long(''.join(["%02x" % byte for byte in arr]), 16)
16+
17+
18+
def base64_to_long(data):
19+
if isinstance(data, six.text_type):
20+
data = data.encode("ascii")
21+
22+
# urlsafe_b64decode will happily convert b64encoded data
23+
_d = base64.urlsafe_b64decode(bytes(data) + b'==')
24+
return int_arr_to_long(struct.unpack('%sB' % len(_d), _d))
425

526

627
def calculate_at_hash(access_token, hash_alg):
@@ -83,3 +104,5 @@ def constant_time_string_compare(a, b):
83104
result |= ord(x) ^ ord(y)
84105

85106
return result == 0
107+
108+

tests/algorithms/test_RSA.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@ def test_RSA_key_instance(self):
106106
long(26057131595212989515105618545799160306093557851986992545257129318694524535510983041068168825614868056510242030438003863929818932202262132630250203397069801217463517914103389095129323580576852108653940669240896817348477800490303630912852266209307160550655497615975529276169196271699168537716821419779900117025818140018436554173242441334827711966499484119233207097432165756707507563413323850255548329534279691658369466534587631102538061857114141268972476680597988266772849780811214198186940677291891818952682545840788356616771009013059992237747149380197028452160324144544057074406611859615973035412993832273216732343819),
107107
).public_key(default_backend())
108108

109-
CryptographyRSAKey(key, ALGORITHMS.RS256)
109+
pubkey = CryptographyRSAKey(key, ALGORITHMS.RS256)
110+
pem = pubkey.to_pem()
111+
assert pem.startswith('-----BEGIN PUBLIC KEY-----')
110112

111113
def test_RSA_jwk(self):
112114
d = {
@@ -133,7 +135,7 @@ def test_bad_cert(self):
133135

134136
def test_signing_parity(self):
135137
key1 = RSAKey(private_key, ALGORITHMS.RS256)
136-
public_key = key1.prepared_key.publickey().exportKey().decode('utf-8')
138+
public_key = key1.public_key().to_pem().decode('utf-8')
137139
vkey1 = RSAKey(public_key, ALGORITHMS.RS256)
138140
key2 = CryptographyRSAKey(private_key, ALGORITHMS.RS256)
139141
vkey2 = CryptographyRSAKey(public_key, ALGORITHMS.RS256)

tests/algorithms/test_cryptography_EC.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ class TestCryptographyECAlgorithm:
1818

1919
def test_EC_key(self):
2020
key = ecdsa.SigningKey.from_pem(private_key)
21-
CryptographyECKey(key, ALGORITHMS.ES256)
21+
k = CryptographyECKey(key, ALGORITHMS.ES256)
22+
23+
print(repr(k.to_pem().strip()))
24+
print(repr(private_key.strip()))
25+
assert k.to_pem().strip() == private_key.strip()
26+
public_pem = k.public_key().to_pem()
27+
public_key = CryptographyECKey(public_pem, ALGORITHMS.ES256)
2228

2329
def test_string_secret(self):
2430
key = 'secret'

0 commit comments

Comments
 (0)