I have a secure web socket server implemented in python 3 running on a RaspberryPI device at address RASPI_ADDRESS, exposed on port 8000. On the RaspberryPI device, this is what the ssl version is shown as:
>>> import ssl
>>> print(ssl.OPENSSL_VERSION)
OpenSSL 1.1.1d 10 Sep 2019
For testing purposes, I am using a self signed certificate, generated with openssl: certificate file cert.pem with the companion private key in key.pem.
On the client side, I am on a Windows machine and I implemented the client as follows (the same cert.pem file from above is available here as a local copy):
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)
I am getting this exception on ws.connect(...):
Exception: [SSL] PEM lib (_ssl.c:4065)
(If I connect in a non-secure way using "ws://...", it works)
Unfortunately, I am not getting many relevant results when searching for this error. I have tried to also provide the private key in the sslopt as well ("keyfile": "key.pem"), but then the script seems caught in some sync blocking - no exception, nothing listed on the screen, but also nothing received on the server side.
Any pointers as to what I am doing wrong?
CodePudding user response:
In the end, I solved it by rewriting the server and client using the websockets library: https://pypi.org/project/websockets/
Perhaps it would also have run with the websocket-client library https://pypi.org/project/websocket-client/ I was using before, but the docs were partly inconsistent and confusing. Writing here a simplified working solution for future reference, in the form of a dummy echo server.
Server running on RasPI (visible in LAN at IP address 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())
Note the "0.0.0.0" host IP in websockets.serve()! If we set this to "localhost", the client will see a stack trace ending with this error:
ConnectionRefusedError: [WinError 1225] The remote computer refused the network connection
Client running on Windows machine:
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())
This got me at least a reaction compared to the original implementation, as then I ran into this error:
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: IP address mismatch, certificate is not valid for *RASPI_IP*. (_ssl.c:1129)
This is solved by generating a certificate with SAN instead of only CN: https://serverfault.com/a/880809
Additionally, I combined the certificate and the key into one file: cat key.pem cert.pem > key_cert.pem