Repository: pyca/cryptography

Issue:
Password too long triggers an AssertionError when loading keys on the OpenSSL backend
``` pycon
>>> serialization.load_pem_traditional_openssl_private_key(f.read(), "\x00" * 10000000, default_backend()._backends[1])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "cryptography/hazmat/primitives/serialization.py", line 19, in load_pem_traditional_openssl_private_key
    data, password
  File "cryptography/hazmat/backends/openssl/backend.py", line 766, in load_traditional_openssl_pem_private_key
    return self.load_pkcs8_pem_private_key(data, password)
  File "cryptography/hazmat/backends/openssl/backend.py", line 781, in load_pkcs8_pem_private_key
    self._handle_key_loading_error(password)
  File "cryptography/hazmat/backends/openssl/backend.py", line 814, in _handle_key_loading_error
    assert not password
AssertionError
```

This happens because in the PEM password cb:

``` py
        def pem_password_cb(buf, size, writing, userdata):
            pem_password_cb.called += 1

            if not password or len(password) >= size:
                return 0
            else:
                # ...
```

So we say that there isn't a password, when there is.



Discussion:
Just how long was this password?

In this example, I was just testing a ridiculously long password (100,000 bytes or something), I was actually hunting for memory corruption bugs :-)

Good find :) The buffer is fixed at 1024 bytes in OpenSSL. Looks like it might be tricky to handle this case nicely. I guess replacing the AssertionError with a ValueError or something might good?

Yeah, I think we'll have to pass this info outside the normal OpenSSL error
stuff (like we do with `.called`). Also, just thinking outloud here, but
passwords with `NUL` bytes probably do weird things because it's a `NUL`
terminated string.

On Wed, Jul 9, 2014 at 7:43 AM, Alex Stapleton notifications@github.com
wrote:

> Good find :) The buffer is fixed at 1024 bytes in OpenSSL. Looks like it
> might be tricky to handle this case nicely. I guess replacing the
> AssertionError with a ValueError or something might good?
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/pyca/cryptography/issues/1235#issuecomment-48480326.

## 

"I disapprove of what you say, but I will defend to the death your right to
say it." -- Evelyn Beatrice Hall (summarizing Voltaire)
"The people's good is the highest law." -- Cicero
GPG Key fingerprint: 125F 5C67 DFE9 4084

Probably worth writing a test for NUL bytes at least. I think OpenSSL does the right thing though.

I'm fixing this :)


Resolving patch:
diff --git a/cryptography/hazmat/backends/openssl/backend.py b/cryptography/hazmat/backends/openssl/backend.py
--- a/cryptography/hazmat/backends/openssl/backend.py
+++ b/cryptography/hazmat/backends/openssl/backend.py
@@ -490,14 +490,24 @@ def _pem_password_cb(self, password):
         def pem_password_cb(buf, size, writing, userdata):
             pem_password_cb.called += 1
 
-            if not password or len(password) >= size:
+            if not password:
+                pem_password_cb.exception = TypeError(
+                    "Password was not given but private key is encrypted."
+                )
                 return 0
-            else:
+            elif len(password) < size:
                 pw_buf = self._ffi.buffer(buf, size)
                 pw_buf[:len(password)] = password
                 return len(password)
+            else:
+                pem_password_cb.exception = ValueError(
+                    "Passwords longer than {0} bytes are not supported "
+                    "by this backend.".format(size - 1)
+                )
+                return 0
 
         pem_password_cb.called = 0
+        pem_password_cb.exception = None
 
         return (
             self._ffi.callback("int (char *, int, int, void *)",
@@ -786,7 +796,12 @@ def _load_key(self, openssl_read_func, convert_func, data, password):
         )
 
         if evp_pkey == self._ffi.NULL:
-            self._handle_key_loading_error(password)
+            if password_func.exception is not None:
+                errors = self._consume_errors()
+                assert errors
+                raise password_func.exception
+            else:
+                self._handle_key_loading_error()
 
         evp_pkey = self._ffi.gc(evp_pkey, self._lib.EVP_PKEY_free)
 
@@ -801,28 +816,12 @@ def _load_key(self, openssl_read_func, convert_func, data, password):
 
         return convert_func(evp_pkey)
 
-    def _handle_key_loading_error(self, password):
+    def _handle_key_loading_error(self):
         errors = self._consume_errors()
+
         if not errors:
             raise ValueError("Could not unserialize key data.")
 
-        if (
-            errors[0][1:] == (
-                self._lib.ERR_LIB_PEM,
-                self._lib.PEM_F_PEM_DO_HEADER,
-                self._lib.PEM_R_BAD_PASSWORD_READ
-            )
-        ) or (
-            errors[0][1:] == (
-                self._lib.ERR_LIB_PEM,
-                self._lib.PEM_F_PEM_READ_BIO_PRIVATEKEY,
-                self._lib.PEM_R_BAD_PASSWORD_READ
-            )
-        ):
-            assert not password
-            raise TypeError(
-                "Password was not given but private key is encrypted.")
-
         elif errors[0][1:] == (
             self._lib.ERR_LIB_EVP,
             self._lib.EVP_F_EVP_DECRYPTFINAL_EX,


Test patch:
diff --git a/tests/hazmat/backends/test_openssl.py b/tests/hazmat/backends/test_openssl.py
--- a/tests/hazmat/backends/test_openssl.py
+++ b/tests/hazmat/backends/test_openssl.py
@@ -13,6 +13,7 @@
 
 from __future__ import absolute_import, division, print_function
 
+import os
 import subprocess
 import sys
 import textwrap
@@ -33,7 +34,7 @@
 from cryptography.hazmat.primitives.ciphers.modes import CBC, CTR
 from cryptography.hazmat.primitives.interfaces import BlockCipherAlgorithm
 
-from ...utils import raises_unsupported_algorithm
+from ...utils import load_vectors_from_file, raises_unsupported_algorithm
 
 
 @utils.register_interface(interfaces.Mode)
@@ -464,7 +465,7 @@ def __init__(self):
 
 
 class TestOpenSSLSerialisationWithOpenSSL(object):
-    def test_password_too_long(self):
+    def test_pem_password_cb_buffer_too_small(self):
         ffi_cb, cb = backend._pem_password_cb(b"aa")
         assert cb(None, 1, False, None) == 0
 
@@ -473,6 +474,22 @@ def test_unsupported_evp_pkey_type(self):
         with raises_unsupported_algorithm(None):
             backend._evp_pkey_to_private_key(key)
 
+    def test_very_long_pem_serialization_password(self):
+        password = "x" * 1024
+
+        with pytest.raises(ValueError):
+            load_vectors_from_file(
+                os.path.join(
+                    "asymmetric", "Traditional_OpenSSL_Serialization",
+                    "key1.pem"
+                ),
+                lambda pemfile: (
+                    backend.load_traditional_openssl_pem_private_key(
+                        pemfile.read().encode(), password
+                    )
+                )
+            )
+
 
 class TestOpenSSLNoEllipticCurve(object):
     def test_elliptic_curve_supported(self, monkeypatch):