Skip to content

Commit

Permalink
Pull Request coderanger#49 from cread - Remove OpenSSL binary dependa…
Browse files Browse the repository at this point in the history
…ncy, use pure python RSA instead
  • Loading branch information
freimer committed Apr 8, 2016
1 parent 5270648 commit f7ffdf6
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 260 deletions.
19 changes: 9 additions & 10 deletions chef/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from chef.auth import sign_request
from chef.exceptions import ChefServerError
from chef.rsa import Key
from chef.key import Key
from chef.utils import json
from chef.utils.file import walk_backwards

Expand Down Expand Up @@ -201,15 +201,14 @@ def request(self, method, path, headers={}, data=None):
try:
response = self._request(method, self.url + path, data, dict(
(k.capitalize(), v) for k, v in six.iteritems(request_headers)))
except six.moves.urllib.error.HTTPError as e:
e.content = e.read()
try:
e.content = json.loads(e.content.decode())
raise ChefServerError.from_error(e.content['error'], code=e.code)
except ValueError:
pass
raise e
return response
except requests.ConnectionError as e:
raise ChefServerError(e.message)

if response.ok:
return response

raise ChefServerError.from_error(response.reason, code=response.status_code)


def api_request(self, method, path, headers={}, data=None):
headers = dict((k.lower(), v) for k, v in six.iteritems(headers))
Expand Down
5 changes: 3 additions & 2 deletions chef/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ def sign_request(key, http_method, path, body, host, timestamp, user_id):

# Create RSA signature
req = canonical_request(http_method, path, hashed_body, timestamp, user_id)
sig = _ruby_b64encode(key.private_encrypt(req))
for i, line in enumerate(sig):
sig = key.sign(req)
enc_sig = _ruby_b64encode(sig)
for i, line in enumerate(enc_sig):
headers['x-ops-authorization-%s'%(i+1)] = line
return headers
130 changes: 130 additions & 0 deletions chef/key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import six
import sys

import rsa


def _pad_sig(message, length):
""" Simplified padding of the message.
Return message is length bytes and of the format:
00 01 <FF pading> 00 message
"""

maxlen = length - 4
msglen = len(message)

if msglen > maxlen:
raise OverflowError('{} byte message > {} message limit'.format(msglen, maxlen))

padlen = length - msglen - 3

return b''.join([
b'\x00\x01',
padlen * b'\xff',
b'\x00',
message.encode('latin1')
])


class VerificationError(Exception):
"""An error in Message Verification."""

def __init__(self, message, *args):
message = message % args
super(VerificationError, self).__init__(message)


class SSLError(Exception):
"""An error in Key Management."""

def __init__(self, message, *args):
message = message % args
super(SSLError, self).__init__(message)


class Key(object):
"""An RSA key handler"""

def __init__(self, fp=None):
self.key = None
self.public = False
if not fp:
return
if isinstance(fp, six.binary_type) and fp.startswith(b'-----BEGIN'):
# PEM formatted text
self.raw = fp
elif isinstance(fp, six.string_types) and fp.startswith('-----BEGIN'):
# PEM formatted text
self.raw = fp
elif isinstance(fp, six.string_types):
self.raw = open(fp, 'rb').read()
else:
self.raw = fp.read()

if sys.version_info[0] < 3:
if type(self.raw) is not unicode:
self.raw = unicode(self.raw)

self._load_key()

def _load_key(self):
try:
self.key = rsa.PrivateKey.load_pkcs1(self.raw)
except ValueError:
self.key = rsa.PublicKey.load_pkcs1(self.raw)
self.public = True
except:
raise ValueError("'{}' is not a valid RSA key".format(self.raw))

@classmethod
def generate(cls, size=1024):
self = cls()
(_, self.key) = rsa.newkeys(size)
return self

def sign(self, message):
""" Simplified signature compatible with `openssl rsautl -sign`
Signing logic pulled from the rsa lib, but does not add the asn1 before padding.
"""

if self.public:
raise SSLError('can not sign a message using a public key')

keylength = rsa.common.byte_size(self.key.n)
padded = _pad_sig(message, keylength)
payload = rsa.transform.bytes2int(padded)
encrypted = rsa.core.encrypt_int(payload, self.key.d, self.key.n)
block = rsa.transform.int2bytes(encrypted, keylength)

return block

def verify(self, message, sig):
""" Emulate `openssl rsautl -verify` """

blocksize = rsa.common.byte_size(self.key.n)
encrypted = rsa.transform.bytes2int(sig)
decrypted = rsa.core.decrypt_int(encrypted, self.key.e, self.key.n)
clearsig = rsa.transform.int2bytes(decrypted, blocksize)

# If we can't find the signature marker, verification failed.
if clearsig[0:2] != b'\x00\x01':
raise VerificationError("Verification failed, sig starts with '{}'".format(clearsig[0:2]))

padded = _pad_sig(message, blocksize)
if padded != clearsig:
raise VerificationError('Verification failed')

return True

def private_export(self):
if self.public:
raise SSLError('private method cannot be used on a public key')

return self.key.save_pkcs1('PEM')

def public_export(self):
return rsa.PublicKey(self.key.n, self.key.e).save_pkcs1('PEM')
Loading

0 comments on commit f7ffdf6

Please sign in to comment.