Repository: googleapis/google-cloud-python

Issue:
Consider removing hard dependency on PyOpenSSL
Authenticating service accounts with a JSON private key should not require installing PyOpenSSL.

More specifically, when [`isinstance(credentials, service_account._ServiceAccountCredentials)`](https://github.com/GoogleCloudPlatform/gcloud-python/blob/f577b3079333273f0be3393de4f3767853f95add/gcloud/storage/connection.py#L65) is `True`, PyOpenSSL is not required.

Given that PyOpenSSL requires OpenSSL, minimizing the dependency is beneficial.



Discussion:
:+1: The `oauth2client` dependency should be sufficient. It's a dep that's been "unexamined" for some time. The last time it was touched was in #419 (my PR). It's used to convert a PKCS12 key to a PM payload for signing a URL.

``` python
        # Take our PKCS12 (.p12) key and make it into a RSA key we can use.
        pkcs12 = crypto.load_pkcs12(
            base64.b64decode(credentials.private_key),
            'notasecret')
        pem_text = crypto.dump_privatekey(
            crypto.FILETYPE_PEM, pkcs12.get_privatekey())
```

The function `storage.connection._get_pem_key` is **only used** by the function `storage.connection._get_signed_query_params` which in turn is **only used** by `storage.connection.Connection.generate_signed_url`.

The only other place we use "crypto" is in our regression tests, checking an MD5 hash against a stored file.

---

@craigcitro What do you think about moving some of the behavior of `_get_pem_key` and `_get_signed_query_params` into google/oauth2client? We've discussed this before, but signed a URL is a generic thing people may do.

@dhermes SGTM, especially if you move some tests in with it. `;)`

google/oauth2client#115 created to offload the dependency onto `oauth2client`.

That library makes me sad every time I read the source :( But this is true of a **lot** of popular libraries, so meh.

I'm wondering if we could also ditch the `PyCrypto` dependency? @s3ththompson I don't have a lot of expertise here, is one easier to deal with than the other?

Since PyCrypto is a pure-python library, there's not really a need to ditch it as a dependency.  The problem with PyOpenSSL is just that it depends on installing OpenSSL a (non-python) binary.

Ahhh! Very nice.

So google/oauth2client#115 has been cut, so fixing this can now be done on the next release of `oauth2client`.


Resolving patch:
diff --git a/gcloud/storage/acl.py b/gcloud/storage/acl.py
--- a/gcloud/storage/acl.py
+++ b/gcloud/storage/acl.py
@@ -108,11 +108,11 @@ def __str__(self):
         if not self.identifier:
             return str(self.type)
         else:
-            return '{self.type}-{self.identifier}'.format(self=self)
+            return '{acl.type}-{acl.identifier}'.format(acl=self)
 
     def __repr__(self):
-        return '<ACL Entity: {self} ({roles})>'.format(
-            self=self, roles=', '.join(self.roles))
+        return '<ACL Entity: {acl} ({roles})>'.format(
+            acl=self, roles=', '.join(self.roles))
 
     def get_roles(self):
         """Get the list of roles permitted by this entity.
diff --git a/gcloud/storage/connection.py b/gcloud/storage/connection.py
--- a/gcloud/storage/connection.py
+++ b/gcloud/storage/connection.py
@@ -23,8 +23,8 @@
 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 import crypt
 from oauth2client import service_account
 import pytz
 
@@ -57,11 +57,8 @@ def _get_pem_key(credentials):
     """
     if isinstance(credentials, client.SignedJwtAssertionCredentials):
         # Take our PKCS12 (.p12) key and make it into a RSA key we can use.
-        pkcs12 = crypto.load_pkcs12(
-            base64.b64decode(credentials.private_key),
-            'notasecret')
-        pem_text = crypto.dump_privatekey(
-            crypto.FILETYPE_PEM, pkcs12.get_privatekey())
+        pem_text = crypt.pkcs12_key_as_pem(credentials.private_key,
+                                           credentials.private_key_password)
     elif isinstance(credentials, service_account._ServiceAccountCredentials):
         pem_text = credentials._private_key_pkcs8_text
     else:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -13,10 +13,9 @@
 
 REQUIREMENTS = [
     'httplib2',
-    'oauth2client',
+    'oauth2client >= 1.4.6',
     'protobuf >= 2.5.0',
     'pycrypto',
-    'pyopenssl',
     'pytz',
     'six',
 ]


Test patch:
diff --git a/gcloud/storage/test_connection.py b/gcloud/storage/test_connection.py
--- a/gcloud/storage/test_connection.py
+++ b/gcloud/storage/test_connection.py
@@ -767,17 +767,23 @@ def test_bad_argument(self):
         self.assertRaises(TypeError, self._callFUT, None)
 
     def test_signed_jwt_for_p12(self):
+        import base64
         from oauth2client import client
         from gcloud._testing import _Monkey
         from gcloud.storage import connection as MUT
 
         scopes = []
+        PRIVATE_KEY = 'dummy_private_key_text'
         credentials = client.SignedJwtAssertionCredentials(
-            'dummy_service_account_name', 'dummy_private_key_text', scopes)
-        crypto = _Crypto()
+            'dummy_service_account_name', PRIVATE_KEY, scopes)
+        crypt = _Crypt()
         rsa = _RSA()
-        with _Monkey(MUT, crypto=crypto, RSA=rsa):
+        with _Monkey(MUT, crypt=crypt, RSA=rsa):
             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__')
 
     def test_service_account_via_json_key(self):
@@ -816,7 +822,6 @@ def test_wrong_type(self):
         from gcloud._testing import _Monkey
         from gcloud.storage import connection as MUT
 
-        crypto = _Crypto()
         pkcs_v1_5 = _PKCS1_v1_5()
         rsa = _RSA()
         sha256 = _SHA256()
@@ -827,7 +832,7 @@ def _get_pem_key(credentials):
         BAD_CREDENTIALS = None
         EXPIRATION = '100'
         SIGNATURE_STRING = 'dummy_signature'
-        with _Monkey(MUT, crypto=crypto, RSA=rsa, PKCS1_v1_5=pkcs_v1_5,
+        with _Monkey(MUT, RSA=rsa, PKCS1_v1_5=pkcs_v1_5,
                      SHA256=sha256, _get_pem_key=_get_pem_key):
             self.assertRaises(NameError, self._callFUT,
                               BAD_CREDENTIALS, EXPIRATION, SIGNATURE_STRING)
@@ -837,17 +842,21 @@ def _run_test_with_credentials(self, credentials, account_name):
         from gcloud._testing import _Monkey
         from gcloud.storage import connection as MUT
 
-        crypto = _Crypto()
+        crypt = _Crypt()
         pkcs_v1_5 = _PKCS1_v1_5()
         rsa = _RSA()
         sha256 = _SHA256()
 
         EXPIRATION = '100'
         SIGNATURE_STRING = 'dummy_signature'
-        with _Monkey(MUT, crypto=crypto, RSA=rsa, PKCS1_v1_5=pkcs_v1_5,
+        with _Monkey(MUT, crypt=crypt, RSA=rsa, PKCS1_v1_5=pkcs_v1_5,
                      SHA256=sha256):
             result = self._callFUT(credentials, EXPIRATION, SIGNATURE_STRING)
 
+        if crypt._pkcs12_key_as_pem_called:
+            self.assertEqual(crypt._private_key_text,
+                             base64.b64encode('dummy_private_key_text'))
+            self.assertEqual(crypt._private_key_password, 'notasecret')
         self.assertEqual(sha256._signature_string, SIGNATURE_STRING)
         SIGNED = base64.b64encode('DEADBEEF')
         expected_query = {
@@ -900,20 +909,14 @@ def request(self, **kw):
         return self._response, self._content
 
 
-class _Crypto(object):
-
-    FILETYPE_PEM = 'pem'
-    _loaded = _dumped = None
-
-    def load_pkcs12(self, buffer, passphrase):
-        self._loaded = (buffer, passphrase)
-        return self
+class _Crypt(object):
 
-    def get_privatekey(self):
-        return '__PKCS12__'
+    _pkcs12_key_as_pem_called = False
 
-    def dump_privatekey(self, type, pkey, cipher=None, passphrase=None):
-        self._dumped = (type, pkey, cipher, passphrase)
+    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__'