Repository: urllib3/urllib3

Issue:
WIP: Ensure PyOpenSSLContext.load_verify_locations raises ssl.SSLError
Without this patch, an `OpenSSL.SSL.Error` is raised when `PyOpenSSLContext.load_verify_locations` fails:
```
$ python -c "import os; import urllib3; import urllib3.contrib.pyopenssl; urllib3.contrib.pyopenssl.inject_into_urllib3(); urllib3.util.ssl_wrap_socket(None, ca_certs=os.devnull)"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/tmp/urllib3/local/lib/python2.7/site-packages/urllib3/util/ssl_.py", line 321, in ssl_wrap_socket
    context.load_verify_locations(ca_certs, ca_cert_dir)
  File "/tmp/urllib3/local/lib/python2.7/site-packages/urllib3/contrib/pyopenssl.py", line 428, in load_verify_locations
    self._ctx.load_verify_locations(cafile, capath)
  File "/tmp/urllib3/local/lib/python2.7/site-packages/OpenSSL/SSL.py", line 781, in load_verify_locations
    _raise_current_error()
  File "/tmp/urllib3/local/lib/python2.7/site-packages/OpenSSL/_util.py", line 53, in exception_from_error_queue
    raise exception_type(errors)
OpenSSL.SSL.Error: []
```

With this patch, an `ssl.SSLError` is raised, in the same way [`do_handshake` errors are handled](https://github.com/pilou-/urllib3/blob/f17a64dbd052be6cdbdbd8b03f01ad0bc09a5ab3/src/urllib3/contrib/pyopenssl.py#L461-L462):
```
$ python -c "import os; import urllib3; import urllib3.contrib.pyopenssl; urllib3.contrib.pyopenssl.inject_into_urllib3(); urllib3.util.ssl_wrap_socket(None, ca_certs=os.devnull)"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/home/pilou/src/urllib3/src/urllib3/util/ssl_.py", line 313, in ssl_wrap_socket
    raise SSLError(e)
urllib3.exceptions.SSLError: ('unable to load trusted certificates: Error([('x509 certificate routines', 'X509_load_cert_crl_file', 'no certificate or crl found')],)',)
```

then:
1. this `ssl.SSLError` is caught by [`ssl_wrap_socket`](https://github.com/pilou-/urllib3/blob/f17a64dbd052be6cdbdbd8b03f01ad0bc09a5ab3/src/urllib3/util/ssl_.py#L310-L313)
2. an `urllib3.exceptions.SSLError` error is raised
    * which is the same exception that the one raised when pyOpenSSL isn't available
    * and which is what users of urllib3 library expect (this can be tested with `python -c "import requests, os; requests.get('https://github.com', verify=os.devnull)"`)


Discussion:
# [Codecov](https://codecov.io/gh/urllib3/urllib3/pull/1517?src=pr&el=h1) Report
> Merging [#1517](https://codecov.io/gh/urllib3/urllib3/pull/1517?src=pr&el=desc) into [master](https://codecov.io/gh/urllib3/urllib3/commit/adb358f8e06865406d1f05e581a16cbea2136fbc?src=pr&el=desc) will **increase** coverage by `0.03%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/urllib3/urllib3/pull/1517/graphs/tree.svg?width=650&token=UYKHB2uXDn&height=150&src=pr)](https://codecov.io/gh/urllib3/urllib3/pull/1517?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1517      +/-   ##
==========================================
+ Coverage   64.68%   64.72%   +0.03%     
==========================================
  Files          22       22              
  Lines        2897     2897              
==========================================
+ Hits         1874     1875       +1     
+ Misses       1023     1022       -1
```


| [Impacted Files](https://codecov.io/gh/urllib3/urllib3/pull/1517?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/urllib3/util/ssl\_.py](https://codecov.io/gh/urllib3/urllib3/pull/1517/diff?src=pr&el=tree#diff-c3JjL3VybGxpYjMvdXRpbC9zc2xfLnB5) | `37.06% <0%> (+0.43%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/urllib3/urllib3/pull/1517?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/urllib3/urllib3/pull/1517?src=pr&el=footer). Last update [adb358f...b065bed](https://codecov.io/gh/urllib3/urllib3/pull/1517?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Resolving patch:
diff --git a/src/urllib3/contrib/pyopenssl.py b/src/urllib3/contrib/pyopenssl.py
--- a/src/urllib3/contrib/pyopenssl.py
+++ b/src/urllib3/contrib/pyopenssl.py
@@ -450,9 +450,12 @@ def load_verify_locations(self, cafile=None, capath=None, cadata=None):
             cafile = cafile.encode("utf-8")
         if capath is not None:
             capath = capath.encode("utf-8")
-        self._ctx.load_verify_locations(cafile, capath)
-        if cadata is not None:
-            self._ctx.load_verify_locations(BytesIO(cadata))
+        try:
+            self._ctx.load_verify_locations(cafile, capath)
+            if cadata is not None:
+                self._ctx.load_verify_locations(BytesIO(cadata))
+        except OpenSSL.SSL.Error as e:
+            raise ssl.SSLError("unable to load trusted certificates: %r" % e)
 
     def load_cert_chain(self, certfile, keyfile=None, password=None):
         self._ctx.use_certificate_chain_file(certfile)


Test patch:
diff --git a/test/contrib/test_pyopenssl.py b/test/contrib/test_pyopenssl.py
--- a/test/contrib/test_pyopenssl.py
+++ b/test/contrib/test_pyopenssl.py
@@ -45,6 +45,7 @@ def teardown_module():
     TestSNI,
     TestSocketClosing,
     TestClientCerts,
+    TestSSL,
 )
 
 
diff --git a/test/contrib/test_securetransport.py b/test/contrib/test_securetransport.py
--- a/test/contrib/test_securetransport.py
+++ b/test/contrib/test_securetransport.py
@@ -43,6 +43,7 @@ def teardown_module():
     TestSNI,
     TestSocketClosing,
     TestClientCerts,
+    TestSSL,
 )
 
 
diff --git a/test/with_dummyserver/test_socketlevel.py b/test/with_dummyserver/test_socketlevel.py
--- a/test/with_dummyserver/test_socketlevel.py
+++ b/test/with_dummyserver/test_socketlevel.py
@@ -10,6 +10,7 @@
     ProtocolError,
 )
 from urllib3.response import httplib
+from urllib3.util import ssl_wrap_socket
 from urllib3.util.ssl_ import HAS_SNI
 from urllib3.util import ssl_
 from urllib3.util.timeout import Timeout
@@ -37,6 +38,7 @@ class MimeToolMessage(object):
 from collections import OrderedDict
 import os.path
 from threading import Event
+import os
 import select
 import socket
 import shutil
@@ -1387,6 +1389,13 @@ def socket_handler(listener):
                         pool.request("GET", "/", timeout=SHORT_TIMEOUT)
                     context.load_default_certs.assert_not_called()
 
+    def test_load_verify_locations_exception(self):
+        """
+        Ensure that load_verify_locations raises SSLError for all backends
+        """
+        with pytest.raises(SSLError):
+            ssl_wrap_socket(None, ca_certs=os.devnull)
+
 
 class TestErrorWrapping(SocketDummyServerTestCase):
     def test_bad_statusline(self):