Repository: googleapis/google-cloud-python

Issue:
gcloud-python can't import OpenSSL in App Engine (since it can't be vendored in)
I noticed the depending crypto module was changed since commit 948cccf3. The project was deployed with GAE release 1.9.32 and gcloud-python 0.9.0.

Here are some context info according to your recent comments:
1. How do you vendor in gcloud-python for App Engine?
   As described in GAE documentation: https://cloud.google.com/appengine/docs/python/tools/libraries27#vendoring
2. Which services / gcloud-python subpackages do you use? (I'm curious which imports / import paths your code has been using.)
   I am generating signed url for an object in Google Cloud Storage. Here's the partial stacktrace:
   
   > Traceback (most recent call last):
   > ---- omitted for simple trace) ----
   >     from ext import cloudstorage as storage
   >   File "/base/data/home/apps/s~jackend-0/1.390622100755930629/ext/cloudstorage.py", line 8, in <module>
   >     from gcloud import (
   >   File "/base/data/home/apps/s~jackend-0/1.390622100755930629/lib/gcloud/storage/**init**.py", line 43, in <module>
   >     from gcloud.storage.blob import Blob
   >   File "/base/data/home/apps/s~jackend-0/1.390622100755930629/lib/gcloud/storage/blob.py", line 28, in <module>
   >     from gcloud.credentials import generate_signed_url
   >   File "/base/data/home/apps/s~jackend-0/1.390622100755930629/lib/gcloud/credentials.py", line 22, in <module>
   >     from OpenSSL import crypto
   >   File "/base/data/home/apps/s~jackend-0/1.390622100755930629/lib/OpenSSL/**init**.py", line 8, in <module>
   >     from OpenSSL import rand, crypto, SSL
   >   File "/base/data/home/apps/s~jackend-0/1.390622100755930629/lib/OpenSSL/rand.py", line 11, in <module>
   >     from OpenSSL._util import (
   >   File "/base/data/home/apps/s~jackend-0/1.390622100755930629/lib/OpenSSL/_util.py", line 6, in <module>
   >     from cryptography.hazmat.bindings.openssl.binding import Binding
   >   File "/base/data/home/apps/s~jackend-0/1.390622100755930629/lib/cryptography/hazmat/bindings/openssl/binding.py", line 14, in <module>
   >     from cryptography.hazmat.bindings._openssl import ffi, lib
   > ImportError: dynamic module does not define init function (init_openssl)

I guess this is caused by the [inherent inflexibility](https://cloud.google.com/appengine/docs/python/tools/libraries27) with runtime environment in GAE, and affects #1009 which seems to conflict with this issue.



Discussion:
Moving this ~~back in~~ to #1436

Re-opening, this is a **different** import error than the one in #1436, will rename accordingly

Thank you for your clarification.

2016년 2월 12일 (금) 오전 1:03, Danny Hermes notifications@github.com님이 작성:

> Re-opening, this is a _different_ import error than the one in #1436
> https://github.com/GoogleCloudPlatform/gcloud-python/issues/1436, will
> rename accordingly
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/GoogleCloudPlatform/gcloud-python/issues/1445#issuecomment-182934954
> .

@savvykang-bnnjack Thanks for filing. For now, you can get around this by editing `gcloud/credentials.py`

``` diff
diff --git a/gcloud/credentials.py b/gcloud/credentials.py
index 9874042..ab5151c 100644
--- a/gcloud/credentials.py
+++ b/gcloud/credentials.py
@@ -19,7 +19,10 @@ import datetime
 import six
 from six.moves.urllib.parse import urlencode  # pylint: disable=F0401

-from OpenSSL import crypto
+try:
+    from OpenSSL import crypto
+except ImportError:
+    crypto = None

 from oauth2client import client
 from oauth2client.client import _get_application_default_credential_from_file
```

and then your code should work just fine using `google.appengine.api.app_identity.sign_blob`.


Resolving patch:
diff --git a/gcloud/credentials.py b/gcloud/credentials.py
--- a/gcloud/credentials.py
+++ b/gcloud/credentials.py
@@ -19,7 +19,12 @@
 import six
 from six.moves.urllib.parse import urlencode  # pylint: disable=F0401
 
-from OpenSSL import crypto
+try:
+    from OpenSSL import crypto
+except ImportError:  # pragma: NO COVER
+    # pyOpenSSL can't be installed on App Engine, but it will not
+    # be needed there since app_identity is used.
+    crypto = None
 
 from oauth2client import client
 from oauth2client.client import _get_application_default_credential_from_file
@@ -170,6 +175,7 @@ def _get_pem_key(credentials):
     :rtype: :class:`OpenSSL.crypto.PKey`
     :returns: A PKey object used to sign text.
     :raises: `TypeError` if `credentials` is the wrong type.
+             `EnvironmentError` if `crypto` did not import successfully.
     """
     if isinstance(credentials, client.SignedJwtAssertionCredentials):
         # Take our PKCS12 (.p12) text and convert to PEM text.
@@ -181,6 +187,9 @@ def _get_pem_key(credentials):
         raise TypeError((credentials,
                          'not a valid service account credentials type'))
 
+    if crypto is None:
+        raise EnvironmentError(
+            'pyOpenSSL must be installed to load a private key')
     return crypto.load_privatekey(crypto.FILETYPE_PEM, pem_text)
 
 
@@ -198,6 +207,7 @@ def _get_signature_bytes(credentials, string_to_sign):
 
     :rtype: bytes
     :returns: Signed bytes produced by the credentials.
+    :raises: `EnvironmentError` if `crypto` did not import successfully.
     """
     if isinstance(credentials, _GAECreds):
         _, signed_bytes = app_identity.sign_blob(string_to_sign)
@@ -207,6 +217,10 @@ def _get_signature_bytes(credentials, string_to_sign):
         pkey = _get_pem_key(credentials)
         if not isinstance(string_to_sign, six.binary_type):
             string_to_sign = string_to_sign.encode('utf-8')
+        if crypto is None:
+            raise EnvironmentError(
+                'pyOpenSSL must be installed to sign content using a '
+                'private key')
         return crypto.sign(pkey, string_to_sign, 'SHA256')
 
 


Test patch:
diff --git a/gcloud/test_credentials.py b/gcloud/test_credentials.py
--- a/gcloud/test_credentials.py
+++ b/gcloud/test_credentials.py
@@ -350,6 +350,21 @@ def test_gae_type(self):
         self.assertEqual(signed_bytes, STRING_TO_SIGN)
         self.assertEqual(APP_IDENTITY._strings_signed, [STRING_TO_SIGN])
 
+    def test_without_pyopenssl(self):
+        from gcloud._testing import _Monkey
+        from gcloud import credentials as credentials_mod
+
+        mock_called = []
+        credentials = object()
+
+        def mock_pem_key(local_creds):
+            mock_called.append(local_creds)
+
+        with _Monkey(credentials_mod, crypto=None, _get_pem_key=mock_pem_key):
+            with self.assertRaises(EnvironmentError):
+                self._callFUT(credentials, b'STRING_TO_SIGN')
+            self.assertEqual(mock_called, [credentials])
+
 
 class Test__get_service_account_name(unittest2.TestCase):
 
@@ -520,6 +535,25 @@ def _get_private_key(private_key_pkcs8_text):
                          [(openssl_crypto.FILETYPE_PEM, PRIVATE_TEXT)])
         self.assertEqual(openssl_crypto._signed, [])
 
+    def test_without_pyopenssl(self):
+        from oauth2client import service_account
+        from gcloud._testing import _Monkey
+        from gcloud import credentials as credentials_mod
+
+        PRIVATE_TEXT = 'dummy_private_key_pkcs8_text'
+
+        def _get_private_key(private_key_pkcs8_text):
+            return private_key_pkcs8_text
+
+        with _Monkey(service_account, _get_private_key=_get_private_key):
+            credentials = service_account._ServiceAccountCredentials(
+                'dummy_service_account_id', 'dummy_service_account_email',
+                'dummy_private_key_id', PRIVATE_TEXT, '')
+
+        with _Monkey(credentials_mod, crypto=None):
+            with self.assertRaises(EnvironmentError):
+                self._callFUT(credentials)
+
 
 class Test__get_expiration_seconds(unittest2.TestCase):