#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