diff --git a/chef/api.py b/chef/api.py index 3aaaedd..2d307a5 100644 --- a/chef/api.py +++ b/chef/api.py @@ -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 @@ -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)) diff --git a/chef/auth.py b/chef/auth.py index 1f1d582..43d1e04 100644 --- a/chef/auth.py +++ b/chef/auth.py @@ -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 diff --git a/chef/key.py b/chef/key.py new file mode 100644 index 0000000..5ca0ab3 --- /dev/null +++ b/chef/key.py @@ -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 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') diff --git a/chef/rsa.py b/chef/rsa.py deleted file mode 100644 index 32ad3c0..0000000 --- a/chef/rsa.py +++ /dev/null @@ -1,230 +0,0 @@ -import six -import sys -from ctypes import * - -if sys.platform == 'win32' or sys.platform == 'cygwin': - _eay = CDLL('libeay32.dll') -elif sys.platform == 'darwin': - _eay = CDLL('libcrypto.dylib') -else: - _eay = CDLL('libcrypto.so') - -#unsigned long ERR_get_error(void); -ERR_get_error = _eay.ERR_get_error -ERR_get_error.argtypes = [] -ERR_get_error.restype = c_ulong - -#void ERR_error_string_n(unsigned long e, char *buf, size_t len); -ERR_error_string_n = _eay.ERR_error_string_n -ERR_error_string_n.argtypes = [c_ulong, c_char_p, c_size_t] -ERR_error_string_n.restype = None - -class SSLError(Exception): - """An error in OpenSSL.""" - - def __init__(self, message, *args): - message = message%args - err = ERR_get_error() - if err: - message += ':' - while err: - buf = create_string_buffer(120) - ERR_error_string_n(err, buf, 120) - message += '\n%s'%string_at(buf, 119) - err = ERR_get_error() - super(SSLError, self).__init__(message) - - -#BIO * BIO_new(BIO_METHOD *type); -BIO_new = _eay.BIO_new -BIO_new.argtypes = [c_void_p] -BIO_new.restype = c_void_p - -# BIO *BIO_new_mem_buf(void *buf, int len); -BIO_new_mem_buf = _eay.BIO_new_mem_buf -BIO_new_mem_buf.argtypes = [c_void_p, c_int] -BIO_new_mem_buf.restype = c_void_p - -#BIO_METHOD *BIO_s_mem(void); -BIO_s_mem = _eay.BIO_s_mem -BIO_s_mem.argtypes = [] -BIO_s_mem.restype = c_void_p - -#long BIO_ctrl(BIO *bp,int cmd,long larg,void *parg); -BIO_ctrl = _eay.BIO_ctrl -BIO_ctrl.argtypes = [c_void_p, c_int, c_long, c_void_p] -BIO_ctrl.restype = c_long - -#define BIO_CTRL_RESET 1 /* opt - rewind/zero etc */ -BIO_CTRL_RESET = 1 -##define BIO_CTRL_INFO 3 /* opt - extra tit-bits */ -BIO_CTRL_INFO = 3 - -#define BIO_reset(b) (int)BIO_ctrl(b,BIO_CTRL_RESET,0,NULL) -def BIO_reset(b): - return BIO_ctrl(b, BIO_CTRL_RESET, 0, None) - -##define BIO_get_mem_data(b,pp) BIO_ctrl(b,BIO_CTRL_INFO,0,(char *)pp) -def BIO_get_mem_data(b, pp): - return BIO_ctrl(b, BIO_CTRL_INFO, 0, pp) - -# int BIO_free(BIO *a) -BIO_free = _eay.BIO_free -BIO_free.argtypes = [c_void_p] -BIO_free.restype = c_int -def BIO_free_errcheck(result, func, arguments): - if result == 0: - raise SSLError('Unable to free BIO') -BIO_free.errcheck = BIO_free_errcheck - -#RSA *PEM_read_bio_RSAPrivateKey(BIO *bp, RSA **x, -# pem_password_cb *cb, void *u); -PEM_read_bio_RSAPrivateKey = _eay.PEM_read_bio_RSAPrivateKey -PEM_read_bio_RSAPrivateKey.argtypes = [c_void_p, c_void_p, c_void_p, c_void_p] -PEM_read_bio_RSAPrivateKey.restype = c_void_p - -#RSA *PEM_read_bio_RSAPublicKey(BIO *bp, RSA **x, -# pem_password_cb *cb, void *u); -PEM_read_bio_RSAPublicKey = _eay.PEM_read_bio_RSAPublicKey -PEM_read_bio_RSAPublicKey.argtypes = [c_void_p, c_void_p, c_void_p, c_void_p] -PEM_read_bio_RSAPublicKey.restype = c_void_p - -#int PEM_write_bio_RSAPrivateKey(BIO *bp, RSA *x, const EVP_CIPHER *enc, -# unsigned char *kstr, int klen, -# pem_password_cb *cb, void *u); -PEM_write_bio_RSAPrivateKey = _eay.PEM_write_bio_RSAPrivateKey -PEM_write_bio_RSAPrivateKey.argtypes = [c_void_p, c_void_p, c_void_p, c_char_p, c_int, c_void_p, c_void_p] -PEM_write_bio_RSAPrivateKey.restype = c_int - -#int PEM_write_bio_RSAPublicKey(BIO *bp, RSA *x); -PEM_write_bio_RSAPublicKey = _eay.PEM_write_bio_RSAPublicKey -PEM_write_bio_RSAPublicKey.argtypes = [c_void_p, c_void_p] -PEM_write_bio_RSAPublicKey.restype = c_int - -#int RSA_private_encrypt(int flen, unsigned char *from, -# unsigned char *to, RSA *rsa,int padding); -RSA_private_encrypt = _eay.RSA_private_encrypt -RSA_private_encrypt.argtypes = [c_int, c_void_p, c_void_p, c_void_p, c_int] -RSA_private_encrypt.restype = c_int - -#int RSA_public_decrypt(int flen, unsigned char *from, -# unsigned char *to, RSA *rsa, int padding); -RSA_public_decrypt = _eay.RSA_public_decrypt -RSA_public_decrypt.argtypes = [c_int, c_void_p, c_void_p, c_void_p, c_int] -RSA_public_decrypt.restype = c_int - -RSA_PKCS1_PADDING = 1 -RSA_NO_PADDING = 3 - -# int RSA_size(const RSA *rsa); -RSA_size = _eay.RSA_size -RSA_size.argtypes = [c_void_p] -RSA_size.restype = c_int - -#RSA *RSA_generate_key(int num, unsigned long e, -# void (*callback)(int,int,void *), void *cb_arg); -RSA_generate_key = _eay.RSA_generate_key -RSA_generate_key.argtypes = [c_int, c_ulong, c_void_p, c_void_p] -RSA_generate_key.restype = c_void_p - -##define RSA_F4 0x10001L -RSA_F4 = 0x10001 - -# void RSA_free(RSA *rsa); -RSA_free = _eay.RSA_free -RSA_free.argtypes = [c_void_p] - -class Key(object): - """An OpenSSL RSA key.""" - - 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'-----'): - # PEM formatted text - self.raw = fp - elif isinstance(fp, six.string_types): - self.raw = open(fp, 'rb').read() - else: - self.raw = fp.read() - self._load_key() - - def _load_key(self): - if b'\0' in self.raw: - # Raw string has embedded nulls, treat it as binary data - buf = create_string_buffer(self.raw, len(self.raw)) - else: - buf = create_string_buffer(self.raw) - - bio = BIO_new_mem_buf(buf, len(buf)) - try: - self.key = PEM_read_bio_RSAPrivateKey(bio, 0, 0, 0) - if not self.key: - BIO_reset(bio) - self.public = True - self.key = PEM_read_bio_RSAPublicKey(bio, 0, 0, 0) - if not self.key: - raise SSLError('Unable to load RSA key') - finally: - BIO_free(bio) - - @classmethod - def generate(cls, size=1024, exp=RSA_F4): - self = cls() - self.key = RSA_generate_key(size, exp, None, None) - return self - - def private_encrypt(self, value, padding=RSA_PKCS1_PADDING): - if self.public: - raise SSLError('private method cannot be used on a public key') - if six.PY3 and not isinstance(value, bytes): - buf = create_string_buffer(value.encode(), len(value)) - else: - buf = create_string_buffer(value, len(value)) - size = RSA_size(self.key) - output = create_string_buffer(size) - ret = RSA_private_encrypt(len(buf), buf, output, self.key, padding) - if ret <= 0: - raise SSLError('Unable to encrypt data') - return output.raw[:ret] - - def public_decrypt(self, value, padding=RSA_PKCS1_PADDING): - if six.PY3 and not isinstance(value, bytes): - buf = create_string_buffer(value.encode(), len(value)) - else: - buf = create_string_buffer(value, len(value)) - size = RSA_size(self.key) - output = create_string_buffer(size) - ret = RSA_public_decrypt(len(buf), buf, output, self.key, padding) - if ret <= 0: - raise SSLError('Unable to decrypt data') - if six.PY3 and isinstance(output.raw, bytes): - return output.raw[:ret].decode() - else: - return output.raw[:ret] - - def private_export(self): - if self.public: - raise SSLError('private method cannot be used on a public key') - out = BIO_new(BIO_s_mem()) - PEM_write_bio_RSAPrivateKey(out, self.key, None, None, 0, None, None) - buf = c_char_p() - count = BIO_get_mem_data(out, byref(buf)) - pem = string_at(buf, count) - BIO_free(out) - return pem - - def public_export(self): - out = BIO_new(BIO_s_mem()) - PEM_write_bio_RSAPublicKey(out, self.key) - buf = c_char_p() - count = BIO_get_mem_data(out, byref(buf)) - pem = string_at(buf, count) - BIO_free(out) - return pem - - def __del__(self): - if self.key and RSA_free: - RSA_free(self.key) diff --git a/chef/tests/test_rsa.py b/chef/tests/test_key.py similarity index 73% rename from chef/tests/test_rsa.py rename to chef/tests/test_key.py index fbe5f99..b67cee3 100644 --- a/chef/tests/test_rsa.py +++ b/chef/tests/test_key.py @@ -2,7 +2,7 @@ import unittest2 -from chef.rsa import Key, SSLError +from chef.key import Key, SSLError from chef.tests import TEST_ROOT, skipSlowTest class RSATestCase(unittest2.TestCase): @@ -14,6 +14,10 @@ def test_load_public(self): key = Key(os.path.join(TEST_ROOT, 'client_pub.pem')) self.assertTrue(key.public) + def test_does_not_load_invalid_key(self): + with self.assertRaises(ValueError): + Key(os.path.join(TEST_ROOT, __file__)) + def test_private_export(self): key = Key(os.path.join(TEST_ROOT, 'client.pem')) raw = open(os.path.join(TEST_ROOT, 'client.pem'), 'rb').read() @@ -34,22 +38,6 @@ def test_public_export_pubkey(self): raw = open(os.path.join(TEST_ROOT, 'client_pub.pem'), 'rb').read() self.assertTrue(key.public_export().strip(), raw.strip()) - def test_encrypt_decrypt(self): - key = Key(os.path.join(TEST_ROOT, 'client.pem')) - msg = 'Test string!' - self.assertEqual(key.public_decrypt(key.private_encrypt(msg)), msg) - - def test_encrypt_decrypt_pubkey(self): - key = Key(os.path.join(TEST_ROOT, 'client.pem')) - pubkey = Key(os.path.join(TEST_ROOT, 'client_pub.pem')) - msg = 'Test string!' - self.assertEqual(pubkey.public_decrypt(key.private_encrypt(msg)), msg) - - def test_generate(self): - key = Key.generate() - msg = 'Test string!' - self.assertEqual(key.public_decrypt(key.private_encrypt(msg)), msg) - def test_generate_load(self): key = Key.generate() key2 = Key(key.private_export()) @@ -64,3 +52,9 @@ def test_load_pem_string(self): def test_load_public_pem_string(self): key = Key(open(os.path.join(TEST_ROOT, 'client_pub.pem'), 'rb').read()) self.assertTrue(key.public) + + def test_sign(self): + key = Key.generate() + msg = "Hello Worlds" + sig = key.sign(msg) + self.assertTrue(key.verify(msg, sig)) diff --git a/setup.py b/setup.py index 47ee477..86981b4 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ 'Programming Language :: Python', ], zip_safe = False, - install_requires = ['six>=1.9.0','requests>=2.7.0'], + install_requires = ['six>=1.9.0','requests>=2.7.0', 'rsa>=3.2.3'], tests_require = ['unittest2', 'mock'], test_suite = 'unittest2.collector', )