Repository: pyca/cryptography

Issue:
OpenSSL can return more than one error
Currently our error handling code assumes that there is only ever one error on the stack. This has been true so far, but isn't true in general. For example when you parse a corrupted PEM file you can end up with multiple errors.

`_handle_error` at the very least needs to consume all the errors on each call. I'm not sure what the best way to turn them into actual exceptions when there's more than 1 on the stack though. Some sort of priority list? Some sort of magical meta-exception?

This is blocking #463 



Discussion:
I'm probably going to update #359 with some ideas for 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
@@ -182,12 +182,20 @@ def _handle_error(self, mode):
         if not code and isinstance(mode, GCM):
             raise InvalidTag
         assert code != 0
+
+        # consume any remaining errors on the stack
+        ignored_code = None
+        while ignored_code != 0:
+            ignored_code = self._lib.ERR_get_error()
+
+        # raise the first error we found
+        return self._handle_error_code(code)
+
+    def _handle_error_code(self, code):
         lib = self._lib.ERR_GET_LIB(code)
         func = self._lib.ERR_GET_FUNC(code)
         reason = self._lib.ERR_GET_REASON(code)
-        return self._handle_error_code(lib, func, reason)
 
-    def _handle_error_code(self, lib, func, reason):
         if lib == self._lib.ERR_LIB_EVP:
             if func == self._lib.EVP_F_EVP_ENCRYPTFINAL_EX:
                 if reason == self._lib.EVP_R_DATA_NOT_MULTIPLE_OF_BLOCK_LENGTH:


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
@@ -77,24 +77,44 @@ def test_nonexistent_cipher(self, mode):
 
     def test_handle_unknown_error(self):
         with pytest.raises(InternalError):
-            backend._handle_error_code(0, 0, 0)
+            backend._handle_error_code(0)
 
+        backend._lib.ERR_put_error(backend._lib.ERR_LIB_EVP, 0, 0,
+                                   b"test_openssl.py", -1)
         with pytest.raises(InternalError):
-            backend._handle_error_code(backend._lib.ERR_LIB_EVP, 0, 0)
-
+            backend._handle_error(None)
+
+        backend._lib.ERR_put_error(
+            backend._lib.ERR_LIB_EVP,
+            backend._lib.EVP_F_EVP_ENCRYPTFINAL_EX,
+            0,
+            b"test_openssl.py",
+            -1
+        )
+        with pytest.raises(InternalError):
+            backend._handle_error(None)
+
+        backend._lib.ERR_put_error(
+            backend._lib.ERR_LIB_EVP,
+            backend._lib.EVP_F_EVP_DECRYPTFINAL_EX,
+            0,
+            b"test_openssl.py",
+            -1
+        )
         with pytest.raises(InternalError):
-            backend._handle_error_code(
-                backend._lib.ERR_LIB_EVP,
-                backend._lib.EVP_F_EVP_ENCRYPTFINAL_EX,
-                0
-            )
+            backend._handle_error(None)
+
+    def test_handle_multiple_errors(self):
+        for i in range(10):
+            backend._lib.ERR_put_error(backend._lib.ERR_LIB_EVP, 0, 0,
+                                       b"test_openssl.py", -1)
+
+        assert backend._lib.ERR_peek_error() != 0
 
         with pytest.raises(InternalError):
-            backend._handle_error_code(
-                backend._lib.ERR_LIB_EVP,
-                backend._lib.EVP_F_EVP_DECRYPTFINAL_EX,
-                0
-            )
+            backend._handle_error(None)
+
+        assert backend._lib.ERR_peek_error() == 0
 
     def test_ssl_ciphers_registered(self):
         meth = backend._lib.TLSv1_method()