From 948cccf3edf69bbedc837a2d6b8424276a6fc46d Mon Sep 17 00:00:00 2001 From: Danny Hermes Date: Fri, 1 Jan 2016 17:18:21 -0800 Subject: [PATCH] Swapping PyCrypto for pyOpenSSL. This was done because PyCrypto does not install easily on Windows. pyOpenSSL is managed by PyCA (the Python crypto authority) and has a mature release process. This change was influenced by discussions about #1009. --- gcloud/_testing.py | 2 + gcloud/credentials.py | 29 +++++------ gcloud/storage/_helpers.py | 5 +- gcloud/storage/test__helpers.py | 10 ++-- gcloud/test_credentials.py | 90 +++++++++++++++++---------------- setup.py | 2 +- 6 files changed, 69 insertions(+), 69 deletions(-) diff --git a/gcloud/_testing.py b/gcloud/_testing.py index 9a213e5ac878..185af9557423 100644 --- a/gcloud/_testing.py +++ b/gcloud/_testing.py @@ -20,6 +20,8 @@ class _Monkey(object): def __init__(self, module, **kw): self.module = module + if len(kw) == 0: # pragma: NO COVER + raise ValueError('_Monkey was used with nothing to monkey-patch') self.to_restore = dict([(key, getattr(module, key)) for key in kw]) for key, value in kw.items(): setattr(module, key, value) diff --git a/gcloud/credentials.py b/gcloud/credentials.py index cd61b7ff1955..be56ced0af3d 100644 --- a/gcloud/credentials.py +++ b/gcloud/credentials.py @@ -19,9 +19,8 @@ import six from six.moves.urllib.parse import urlencode # pylint: disable=F0401 -from Crypto.Hash import SHA256 -from Crypto.PublicKey import RSA -from Crypto.Signature import PKCS1_v1_5 +from OpenSSL import crypto + from oauth2client import client from oauth2client.client import _get_application_default_credential_from_file from oauth2client import crypt @@ -148,19 +147,19 @@ def get_for_service_account_p12(client_email, private_key_path, scope=None): def _get_pem_key(credentials): - """Gets RSA key for a PEM payload from a credentials object. + """Gets private key for a PEM payload from a credentials object. :type credentials: :class:`client.SignedJwtAssertionCredentials`, :class:`service_account._ServiceAccountCredentials` - :param credentials: The credentials used to create an RSA key + :param credentials: The credentials used to create a private key for signing text. - :rtype: :class:`Crypto.PublicKey.RSA._RSAobj` - :returns: An RSA object used to sign text. + :rtype: :class:`OpenSSL.crypto.PKey` + :returns: A PKey object used to sign text. :raises: `TypeError` if `credentials` is the wrong type. """ if isinstance(credentials, client.SignedJwtAssertionCredentials): - # Take our PKCS12 (.p12) key and make it into a RSA key we can use. + # Take our PKCS12 (.p12) text and convert to PEM text. pem_text = crypt.pkcs12_key_as_pem(credentials.private_key, credentials.private_key_password) elif isinstance(credentials, service_account._ServiceAccountCredentials): @@ -169,7 +168,7 @@ def _get_pem_key(credentials): raise TypeError((credentials, 'not a valid service account credentials type')) - return RSA.importKey(pem_text) + return crypto.load_privatekey(crypto.FILETYPE_PEM, pem_text) def _get_signature_bytes(credentials, string_to_sign): @@ -179,7 +178,7 @@ def _get_signature_bytes(credentials, string_to_sign): :class:`service_account._ServiceAccountCredentials`, :class:`_GAECreds` :param credentials: The credentials used for signing text (typically - involves the creation of an RSA key). + involves the creation of a PKey). :type string_to_sign: string :param string_to_sign: The string to be signed by the credentials. @@ -191,13 +190,11 @@ def _get_signature_bytes(credentials, string_to_sign): _, signed_bytes = app_identity.sign_blob(string_to_sign) return signed_bytes else: - pem_key = _get_pem_key(credentials) - # Sign the string with the RSA key. - signer = PKCS1_v1_5.new(pem_key) + # Sign the string with the PKey. + pkey = _get_pem_key(credentials) if not isinstance(string_to_sign, six.binary_type): string_to_sign = string_to_sign.encode('utf-8') - signature_hash = SHA256.new(string_to_sign) - return signer.sign(signature_hash) + return crypto.sign(pkey, string_to_sign, 'SHA256') def _get_service_account_name(credentials): @@ -233,7 +230,7 @@ def _get_signed_query_params(credentials, expiration, string_to_sign): :type credentials: :class:`client.SignedJwtAssertionCredentials`, :class:`service_account._ServiceAccountCredentials` - :param credentials: The credentials used to create an RSA key + :param credentials: The credentials used to create a private key for signing text. :type expiration: int or long diff --git a/gcloud/storage/_helpers.py b/gcloud/storage/_helpers.py index 062a8fa205cb..e55fcf179a5f 100644 --- a/gcloud/storage/_helpers.py +++ b/gcloud/storage/_helpers.py @@ -18,8 +18,7 @@ """ import base64 - -from Crypto.Hash import MD5 +from hashlib import md5 class _PropertyMixin(object): @@ -168,7 +167,7 @@ def _base64_md5hash(buffer_object): :param buffer_object: Buffer containing bytes used to compute an MD5 hash (as base64). """ - hash_obj = MD5.new() + hash_obj = md5() _write_buffer_to_hash(buffer_object, hash_obj) digest_bytes = hash_obj.digest() return base64.b64encode(digest_bytes) diff --git a/gcloud/storage/test__helpers.py b/gcloud/storage/test__helpers.py index 2399e4921f3f..815cd58ab65e 100644 --- a/gcloud/storage/test__helpers.py +++ b/gcloud/storage/test__helpers.py @@ -158,13 +158,13 @@ def read(self, block_size): BUFFER = _Buffer([b'', BYTES_TO_SIGN]) MD5 = _MD5(DIGEST_VAL) - with _Monkey(MUT, base64=BASE64, MD5=MD5): + with _Monkey(MUT, base64=BASE64, md5=MD5): SIGNED_CONTENT = self._callFUT(BUFFER) self.assertEqual(BUFFER._block_sizes, [8192, 8192]) self.assertTrue(SIGNED_CONTENT is DIGEST_VAL) self.assertEqual(BASE64._called_b64encode, [DIGEST_VAL]) - self.assertEqual(MD5._new_called, [None]) + self.assertEqual(MD5._called, [None]) self.assertEqual(MD5.hash_obj.num_digest_calls, 1) self.assertEqual(MD5.hash_obj._blocks, [BYTES_TO_SIGN]) @@ -200,10 +200,10 @@ class _MD5(object): def __init__(self, digest_val): self.hash_obj = _MD5Hash(digest_val) - self._new_called = [] + self._called = [] - def new(self, data=None): - self._new_called.append(data) + def __call__(self, data=None): + self._called.append(data) return self.hash_obj diff --git a/gcloud/test_credentials.py b/gcloud/test_credentials.py index 9191b535b9fa..3d68555ce8ce 100644 --- a/gcloud/test_credentials.py +++ b/gcloud/test_credentials.py @@ -247,25 +247,29 @@ def _run_with_fake_crypto(self, credentials, private_key_text, from gcloud import credentials as MUT crypt = _Crypt() - pkcs_v1_5 = _PKCS1_v1_5() - rsa = _RSA() - sha256 = _SHA256() + load_result = object() + sign_result = object() + openssl_crypto = _OpenSSLCrypto(load_result, sign_result) - with _Monkey(MUT, crypt=crypt, RSA=rsa, PKCS1_v1_5=pkcs_v1_5, - SHA256=sha256): + with _Monkey(MUT, crypt=crypt, crypto=openssl_crypto): result = self._callFUT(credentials, string_to_sign) if crypt._pkcs12_key_as_pem_called: self.assertEqual(crypt._private_key_text, base64.b64encode(private_key_text)) self.assertEqual(crypt._private_key_password, 'notasecret') - # sha256._string_to_sign is always bytes. - if isinstance(string_to_sign, six.binary_type): - self.assertEqual(sha256._string_to_sign, string_to_sign) + self.assertEqual(openssl_crypto._loaded, + [(openssl_crypto.FILETYPE_PEM, _Crypt._KEY)]) else: - self.assertEqual(sha256._string_to_sign, - string_to_sign.encode('utf-8')) - self.assertEqual(result, b'DEADBEEF') + self.assertEqual(openssl_crypto._loaded, + [(openssl_crypto.FILETYPE_PEM, private_key_text)]) + + if not isinstance(string_to_sign, six.binary_type): + string_to_sign = string_to_sign.encode('utf-8') + self.assertEqual(openssl_crypto._signed, + [(load_result, string_to_sign, 'SHA256')]) + + self.assertEqual(result, sign_result) def test_p12_type(self): from oauth2client.client import SignedJwtAssertionCredentials @@ -450,14 +454,19 @@ def test_signed_jwt_for_p12(self): credentials = client.SignedJwtAssertionCredentials( 'dummy_service_account_name', PRIVATE_KEY, scopes) crypt = _Crypt() - rsa = _RSA() - with _Monkey(MUT, crypt=crypt, RSA=rsa): + load_result = object() + openssl_crypto = _OpenSSLCrypto(load_result, None) + + with _Monkey(MUT, crypt=crypt, crypto=openssl_crypto): result = self._callFUT(credentials) self.assertEqual(crypt._private_key_text, base64.b64encode(PRIVATE_KEY)) self.assertEqual(crypt._private_key_password, 'notasecret') - self.assertEqual(result, 'imported:__PEM__') + self.assertEqual(result, load_result) + self.assertEqual(openssl_crypto._loaded, + [(openssl_crypto.FILETYPE_PEM, _Crypt._KEY)]) + self.assertEqual(openssl_crypto._signed, []) def test_service_account_via_json_key(self): from oauth2client import service_account @@ -476,12 +485,16 @@ def _get_private_key(private_key_pkcs8_text): 'dummy_service_account_id', 'dummy_service_account_email', 'dummy_private_key_id', PRIVATE_TEXT, scopes) - rsa = _RSA() - with _Monkey(MUT, RSA=rsa): + load_result = object() + openssl_crypto = _OpenSSLCrypto(load_result, None) + + with _Monkey(MUT, crypto=openssl_crypto): result = self._callFUT(credentials) - expected = 'imported:%s' % (PRIVATE_TEXT,) - self.assertEqual(result, expected) + self.assertEqual(result, load_result) + self.assertEqual(openssl_crypto._loaded, + [(openssl_crypto.FILETYPE_PEM, PRIVATE_TEXT)]) + self.assertEqual(openssl_crypto._signed, []) class Test__get_expiration_seconds(unittest2.TestCase): @@ -596,43 +609,32 @@ def SignedJwtAssertionCredentials(self, **kw): class _Crypt(object): _pkcs12_key_as_pem_called = False + _KEY = '__PEM__' def pkcs12_key_as_pem(self, private_key_text, private_key_password): self._pkcs12_key_as_pem_called = True self._private_key_text = private_key_text self._private_key_password = private_key_password - return '__PEM__' + return self._KEY -class _RSA(object): +class _OpenSSLCrypto(object): - _imported = None + FILETYPE_PEM = object() - def importKey(self, pem): - self._imported = pem - return 'imported:%s' % pem + def __init__(self, load_result, sign_result): + self._loaded = [] + self._load_result = load_result + self._signed = [] + self._sign_result = sign_result + def load_privatekey(self, key_type, key_text): + self._loaded.append((key_type, key_text)) + return self._load_result -class _PKCS1_v1_5(object): - - _pem_key = _signature_hash = None - - def new(self, pem_key): - self._pem_key = pem_key - return self - - def sign(self, signature_hash): - self._signature_hash = signature_hash - return b'DEADBEEF' - - -class _SHA256(object): - - _string_to_sign = None - - def new(self, string_to_sign): - self._string_to_sign = string_to_sign - return self + def sign(self, pkey, to_sign, sign_algo): + self._signed.append((pkey, to_sign, sign_algo)) + return self._sign_result class _AppIdentity(object): diff --git a/setup.py b/setup.py index 8be33426642a..88e7008649d7 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ 'httplib2 >= 0.9.1', 'oauth2client >= 1.4.6', 'protobuf == 3.0.0a3', - 'pycrypto', + 'pyOpenSSL', 'six', ]