当 ssl 设置看起来没问题时,为什么我在 Python 中得到 [SSL: CERTIFICATE_VERIFY_FAILED]?

dig*_*emy 2 python ssl ssl-certificate websocket

我正在开发一个 Python 应用程序,通过安全的 websocket 协议与在本地主机上运行的服务进行通信。这是一个示例代码:

import json
import asyncio
import websockets
import ssl
import certifi


ssl_context = ssl.create_default_context()
ssl_context.load_verify_locations(certifi.where())
ssl_context.load_default_certs()

query =  {
    "jsonrpc": "2.0",
    "method": "queryHeadsets",
    "params": {},
    "id": 1
    }
json = json.dumps(query)

async def query(json):

    async with websockets.connect("wss://emotivcortex.com:54321") as ws:
        await ws.send(json)
        response = await ws.recv()
        print(response)

asyncio.get_event_loop().run_until_complete(query(json))
Run Code Online (Sandbox Code Playgroud)

问题是 ssl 握手不断失败并出现以下错误:

ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1056)
Run Code Online (Sandbox Code Playgroud)

我运行的是 Windows 10,Python 3.7.3 64 位

$pip list
Package    Version
---------- --------
certifi    2019.3.9
pip        19.0.3
setuptools 40.8.0
websockets 7.0
Run Code Online (Sandbox Code Playgroud)

我已经检查了服务提供的证书。它似乎有效并由 COMODO 签名。我试过了:

ssl_context = ssl.create_default_context()
ssl_context.load_verify_locations(certifi.where())
print(ssl.get_default_verify_paths())
print(ssl_context.cert_store_stats())
ssl_context.load_default_certs()
print(ssl_context.get_ca_certs())
Run Code Online (Sandbox Code Playgroud)

并发现有几个可用于 python 的 COMODO CA 证书。但是我仍然收到错误。

如果有帮助,这里是完整的错误消息:

SSL handshake failed on verifying the certificate
protocol: <asyncio.sslproto.SSLProtocol object at 0x0000020C11283048>
transport: <_SelectorSocketTransport fd=508 read=polling write=<idle, bufsize=0>>
Traceback (most recent call last):
  File "C:\Users\Matyas2\Python\lib\asyncio\sslproto.py", line 625, in _on_handshake_complete
    raise handshake_exc
  File "C:\Users\Matyas2\Python\lib\asyncio\sslproto.py", line 189, in feed_ssldata
    self._sslobj.do_handshake()
  File "C:\Users\Matyas2\Python\lib\ssl.py", line 763, in do_handshake
    self._sslobj.do_handshake()
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1056)
SSL error in data received
protocol: <asyncio.sslproto.SSLProtocol object at 0x0000020C11283048>
transport: <_SelectorSocketTransport closing fd=508 read=idle write=<idle, bufsize=0>>
Traceback (most recent call last):
  File "C:\Users\Matyas2\Python\lib\asyncio\sslproto.py", line 526, in data_received
    ssldata, appdata = self._sslpipe.feed_ssldata(data)
  File "C:\Users\Matyas2\Python\lib\asyncio\sslproto.py", line 189, in feed_ssldata
    self._sslobj.do_handshake()
  File "C:\Users\Matyas2\Python\lib\ssl.py", line 763, in do_handshake
    self._sslobj.do_handshake()
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1056)
Traceback (most recent call last):
  File "test.py", line 37, in <module>
    asyncio.get_event_loop().run_until_complete(query(json))
  File "C:\Users\Matyas2\Python\lib\asyncio\base_events.py", line 584, in run_until_complete
    return future.result()
  File "test.py", line 32, in query
    async with websockets.connect("wss://emotivcortex.com:54321") as ws:
  File "C:\Users\Matyas2\Python\lib\site-packages\websockets\py35\client.py", line 2, in __aenter__
    return await self
  File "C:\Users\Matyas2\Python\lib\site-packages\websockets\py35\client.py", line 12, in __await_impl__
    transport, protocol = await self._creating_connection
  File "C:\Users\Matyas2\Python\lib\asyncio\base_events.py", line 986, in create_connection
    ssl_handshake_timeout=ssl_handshake_timeout)
  File "C:\Users\Matyas2\Python\lib\asyncio\base_events.py", line 1014, in _create_connection_transport
    await waiter
  File "C:\Users\Matyas2\Python\lib\asyncio\sslproto.py", line 526, in data_received
    ssldata, appdata = self._sslpipe.feed_ssldata(data)
  File "C:\Users\Matyas2\Python\lib\asyncio\sslproto.py", line 189, in feed_ssldata
    self._sslobj.do_handshake()
  File "C:\Users\Matyas2\Python\lib\ssl.py", line 763, in do_handshake
    self._sslobj.do_handshake()
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1056)
Run Code Online (Sandbox Code Playgroud)

与 Internet 中服务器的 SSL 连接工作正常。我错过了什么?我究竟做错了什么?

如果需要,我很乐意提供其他信息。

编辑: 该证书适用于 emotivcortex.com,由 COMODO RSA 域验证安全服务器 CA 颁发,因此我认为它不是自签名证书。开放式SSL:

$python -c "import ssl; print(ssl.OPENSSL_VERSION)"
OpenSSL 1.1.0j  20 Nov 2018
Run Code Online (Sandbox Code Playgroud)

dig*_*emy 5

该问题是由缺少中间 CA 证书引起的。

通过检查 OpenSSL 中服务提供的证书,我发现该证书是由“COMODO RSA 域验证安全服务器 CA”颁发的。这个特定机构的 CA 证书实际上并不存在于 python 包的 CA 包中certifi(有不同的 COMODO... 证书)。

解决方案

从 CA 的网页手动下载缺少的 PEM 格式的证书,并将其添加到您的代码中使用的 CA 包中。

此外,应用程序代码中存在一个错误:调用函数时websockets.connect(),传递关键字参数,ssl=ssl_context以便实际使用之前指定的 CA 包。正确的代码如下所示:

import json
import asyncio
import websockets
import ssl
import certifi


ssl_context = ssl.create_default_context()
ssl_context.load_verify_locations(certifi.where())


query =  {
    "jsonrpc": "2.0",
    "method": "queryHeadsets",
    "params": {},
    "id": 1
    }
json = json.dumps(query)

async def query(json):
    
    async with websockets.connect("wss://emotivcortex.com:54321", ssl=ssl_context) as ws:
        await ws.send(json)
        response = await ws.recv()
        print(response)

asyncio.get_event_loop().run_until_complete(query(json))
Run Code Online (Sandbox Code Playgroud)

非常感谢larsksSteffen Ullrich为我指明了正确的方向。