Skip to content

Commit f7ffdf6

Browse files
committed
Pull Request coderanger#49 from cread - Remove OpenSSL binary dependancy, use pure python RSA instead
1 parent 5270648 commit f7ffdf6

File tree

6 files changed

+154
-260
lines changed

6 files changed

+154
-260
lines changed

chef/api.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from chef.auth import sign_request
1616
from chef.exceptions import ChefServerError
17-
from chef.rsa import Key
17+
from chef.key import Key
1818
from chef.utils import json
1919
from chef.utils.file import walk_backwards
2020

@@ -201,15 +201,14 @@ def request(self, method, path, headers={}, data=None):
201201
try:
202202
response = self._request(method, self.url + path, data, dict(
203203
(k.capitalize(), v) for k, v in six.iteritems(request_headers)))
204-
except six.moves.urllib.error.HTTPError as e:
205-
e.content = e.read()
206-
try:
207-
e.content = json.loads(e.content.decode())
208-
raise ChefServerError.from_error(e.content['error'], code=e.code)
209-
except ValueError:
210-
pass
211-
raise e
212-
return response
204+
except requests.ConnectionError as e:
205+
raise ChefServerError(e.message)
206+
207+
if response.ok:
208+
return response
209+
210+
raise ChefServerError.from_error(response.reason, code=response.status_code)
211+
213212

214213
def api_request(self, method, path, headers={}, data=None):
215214
headers = dict((k.lower(), v) for k, v in six.iteritems(headers))

chef/auth.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ def sign_request(key, http_method, path, body, host, timestamp, user_id):
7575

7676
# Create RSA signature
7777
req = canonical_request(http_method, path, hashed_body, timestamp, user_id)
78-
sig = _ruby_b64encode(key.private_encrypt(req))
79-
for i, line in enumerate(sig):
78+
sig = key.sign(req)
79+
enc_sig = _ruby_b64encode(sig)
80+
for i, line in enumerate(enc_sig):
8081
headers['x-ops-authorization-%s'%(i+1)] = line
8182
return headers

chef/key.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import six
2+
import sys
3+
4+
import rsa
5+
6+
7+
def _pad_sig(message, length):
8+
""" Simplified padding of the message.
9+
10+
Return message is length bytes and of the format:
11+
12+
00 01 <FF pading> 00 message
13+
"""
14+
15+
maxlen = length - 4
16+
msglen = len(message)
17+
18+
if msglen > maxlen:
19+
raise OverflowError('{} byte message > {} message limit'.format(msglen, maxlen))
20+
21+
padlen = length - msglen - 3
22+
23+
return b''.join([
24+
b'\x00\x01',
25+
padlen * b'\xff',
26+
b'\x00',
27+
message.encode('latin1')
28+
])
29+
30+
31+
class VerificationError(Exception):
32+
"""An error in Message Verification."""
33+
34+
def __init__(self, message, *args):
35+
message = message % args
36+
super(VerificationError, self).__init__(message)
37+
38+
39+
class SSLError(Exception):
40+
"""An error in Key Management."""
41+
42+
def __init__(self, message, *args):
43+
message = message % args
44+
super(SSLError, self).__init__(message)
45+
46+
47+
class Key(object):
48+
"""An RSA key handler"""
49+
50+
def __init__(self, fp=None):
51+
self.key = None
52+
self.public = False
53+
if not fp:
54+
return
55+
if isinstance(fp, six.binary_type) and fp.startswith(b'-----BEGIN'):
56+
# PEM formatted text
57+
self.raw = fp
58+
elif isinstance(fp, six.string_types) and fp.startswith('-----BEGIN'):
59+
# PEM formatted text
60+
self.raw = fp
61+
elif isinstance(fp, six.string_types):
62+
self.raw = open(fp, 'rb').read()
63+
else:
64+
self.raw = fp.read()
65+
66+
if sys.version_info[0] < 3:
67+
if type(self.raw) is not unicode:
68+
self.raw = unicode(self.raw)
69+
70+
self._load_key()
71+
72+
def _load_key(self):
73+
try:
74+
self.key = rsa.PrivateKey.load_pkcs1(self.raw)
75+
except ValueError:
76+
self.key = rsa.PublicKey.load_pkcs1(self.raw)
77+
self.public = True
78+
except:
79+
raise ValueError("'{}' is not a valid RSA key".format(self.raw))
80+
81+
@classmethod
82+
def generate(cls, size=1024):
83+
self = cls()
84+
(_, self.key) = rsa.newkeys(size)
85+
return self
86+
87+
def sign(self, message):
88+
""" Simplified signature compatible with `openssl rsautl -sign`
89+
90+
Signing logic pulled from the rsa lib, but does not add the asn1 before padding.
91+
92+
"""
93+
94+
if self.public:
95+
raise SSLError('can not sign a message using a public key')
96+
97+
keylength = rsa.common.byte_size(self.key.n)
98+
padded = _pad_sig(message, keylength)
99+
payload = rsa.transform.bytes2int(padded)
100+
encrypted = rsa.core.encrypt_int(payload, self.key.d, self.key.n)
101+
block = rsa.transform.int2bytes(encrypted, keylength)
102+
103+
return block
104+
105+
def verify(self, message, sig):
106+
""" Emulate `openssl rsautl -verify` """
107+
108+
blocksize = rsa.common.byte_size(self.key.n)
109+
encrypted = rsa.transform.bytes2int(sig)
110+
decrypted = rsa.core.decrypt_int(encrypted, self.key.e, self.key.n)
111+
clearsig = rsa.transform.int2bytes(decrypted, blocksize)
112+
113+
# If we can't find the signature marker, verification failed.
114+
if clearsig[0:2] != b'\x00\x01':
115+
raise VerificationError("Verification failed, sig starts with '{}'".format(clearsig[0:2]))
116+
117+
padded = _pad_sig(message, blocksize)
118+
if padded != clearsig:
119+
raise VerificationError('Verification failed')
120+
121+
return True
122+
123+
def private_export(self):
124+
if self.public:
125+
raise SSLError('private method cannot be used on a public key')
126+
127+
return self.key.save_pkcs1('PEM')
128+
129+
def public_export(self):
130+
return rsa.PublicKey(self.key.n, self.key.e).save_pkcs1('PEM')

0 commit comments

Comments
 (0)