Skip to content

Commit

Permalink
Swapping PyCrypto for pyOpenSSL.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dhermes committed Jan 2, 2016
1 parent f220a36 commit ae525ed
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 69 deletions.
2 changes: 2 additions & 0 deletions gcloud/_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
29 changes: 13 additions & 16 deletions gcloud/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand All @@ -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.
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions gcloud/storage/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
"""

import base64

from Crypto.Hash import MD5
from hashlib import md5


class _PropertyMixin(object):
Expand Down Expand Up @@ -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)
10 changes: 5 additions & 5 deletions gcloud/storage/test__helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand Down Expand Up @@ -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


Expand Down
84 changes: 40 additions & 44 deletions gcloud/test_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -450,14 +454,16 @@ 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)

def test_service_account_via_json_key(self):
from oauth2client import service_account
Expand All @@ -476,12 +482,13 @@ 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)


class Test__get_expiration_seconds(unittest2.TestCase):
Expand Down Expand Up @@ -596,43 +603,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):
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
'httplib2 >= 0.9.1',
'oauth2client >= 1.4.6',
'protobuf == 3.0.0a3',
'pycrypto',
'pyOpenSSL',
'six',
]

Expand Down

0 comments on commit ae525ed

Please sign in to comment.