Не удается подключиться к удаленному защищенному серверу веб-сокетов с помощью библиотеки веб-сокетов python 3

#python-3.x #ssl #websocket #openssl

Вопрос:

У меня есть защищенный сервер веб-сокетов, реализованный в python 3, работающий на устройстве RaspberryPi по адресу RASPI_ADDRESS, доступному на порту 8000. На устройстве RaspberryPi это то, что версия ssl отображается как:

 >>> import ssl
>>> print(ssl.OPENSSL_VERSION)
OpenSSL 1.1.1d  10 Sep 2019
 

В целях тестирования я использую самозаверяющий сертификат, созданный с помощью файла сертификата openssl: cert.pem с сопутствующим закрытым ключом в файле key.pem.

На стороне клиента я нахожусь на компьютере с Windows, и я реализовал клиент следующим образом (тот же файл cert.pem, приведенный выше, доступен здесь в виде локальной копии).:

 import ssl
import websocket

ws = websocket.WebSocket(sslopt={"ssl_version": ssl.PROTOCOL_TLSv1, "certfile": "cert.pem"})
try:
    ws.connect("wss://RASPI_ADDRESS:8000")
    ws.send("Hello, Server")
    print(ws.recv())
    ws.close()
except Exception as e:
    print("Exception: ", e)
 

Я получаю это исключение на ws.connect(…):

 Exception:  [SSL] PEM lib (_ssl.c:4065)
 

(Если я подключусь небезопасным способом, используя «ws://…», это сработает)

К сожалению, я не получаю много релевантных результатов при поиске этой ошибки. Я также попытался предоставить закрытый ключ в sslopt («файл ключа»: «key.pem»), но затем сценарий, похоже, попал в некоторую блокировку синхронизации — никаких исключений, ничего не указано на экране, но также ничего не получено на стороне сервера.

Есть какие-нибудь указания на то, что я делаю не так?

Комментарии:

1. certfile Опция предназначена для сертификата в случае сертификатов клиента — в этом случае ему также потребуется закрытый ключ. Скорее всего, вы ищете ca_certs вариант вместо этого.

2. @SteffenUllrich Спасибо, идея звучит неплохо! К сожалению, это, похоже, ничего не меняет.

3. Если вы по-прежнему получаете ту же ошибку, то файл имеет неправильный формат, т. е. это не файл PEM, или концы строк неправильные или похожие. Трудно сказать, не имея дополнительной информации о файле, кроме его имени. Если сейчас вы получаете другую ошибку, то скорректируйте свой вопрос, чтобы отразить, что вы делаете и какую ошибку вы получаете.

4. Извините, мой плохой, это был случай в понедельник утром :-/ Я больше не получаю ту же ошибку; на самом деле, я вообще не получаю никаких ошибок, но я также не вижу ничего полученного на стороне сервера. Сценарий, похоже, находится в том же состоянии блокировки синхронизации, о котором я упоминал в своем посте при отправке ключевого файла.

5. Вы уверены, что сервер wss вообще работает правильно, т. Е. Что это а) действительно websocket, а не обычный сокет, и б) действительно wss, а не ws? Как вы проверили это?

Ответ №1:

В конце концов, я решил эту проблему, переписав сервер и клиент с помощью библиотеки websockets: https://pypi.org/project/websockets/

Возможно, он также работал бы с клиентской библиотекой websocket https://pypi.org/project/websocket-client/ Я использовал раньше, но документы были частично непоследовательными и сбивающими с толку. Напишу здесь упрощенное рабочее решение для дальнейшего использования в виде фиктивного эхо-сервера.

Сервер, работающий на RasPi (виден в локальной сети по IP-адресу RASPI_IP)

 import asyncio
import pathlib
import ssl
import websockets

async def hello(websocket, path):
    name = await websocket.recv()
    print(f"<<< {name}")

    greeting = f"Hello {name}!"

    await websocket.send(greeting)
    print(f">>> {greeting}")

ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
localhost_pem = pathlib.Path(__file__).with_name("key_cert.pem")
ssl_context.load_cert_chain(localhost_pem)

async def main():
    async with websockets.serve(hello, "0.0.0.0", 8765, ssl=ssl_context):
        await asyncio.Future()  # run forever

asyncio.run(main())
 

Обратите внимание на IP-адрес хоста «0.0.0.0» в websockets.serve()! Если мы установим значение «localhost», клиент увидит трассировку стека, заканчивающуюся этой ошибкой:

 ConnectionRefusedError: [WinError 1225] The remote computer refused the network connection
 

Клиент, работающий на компьютере с Windows:

 import asyncio
import pathlib
import ssl
import websockets

ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
localhost_pem = pathlib.Path(__file__).with_name("key_cert.pem")
ssl_context.load_verify_locations(localhost_pem)
uri_linux = "wss://RASPI_IP:8765"

async def hello():
    uri = uri_linux
    async with websockets.connect(uri, ssl=ssl_context) as websocket:
        name = input("What's your name? ")

        await websocket.send(name)
        print(f">>> {name}")

        greeting = await websocket.recv()
        print(f"<<< {greeting}")

asyncio.run(hello())
 

Это вызвало у меня, по крайней мере, реакцию по сравнению с первоначальной реализацией, так как затем я столкнулся с этой ошибкой:

 ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: IP address mismatch, certificate is not valid for *RASPI_IP*. (_ssl.c:1129)
 

Это решается путем создания сертификата с SAN вместо только CN: https://serverfault.com/a/880809
Кроме того, я объединил сертификат и ключ в один файл: cat key.pem cert.pem > key_cert.pem