Secure Websockets with ESP8266 and Arduino
This last week I've been working on connecting an ESP8266 to Plaza, so it could be used to stream sensor data to the platform (and back).
Along the way I learned a bit of Arduino. The first hour you get to connect the tiny board to a WiFi and write some C code. After that you can keep trying until everything gets to work.
The goal of this post is to show the steps needed to connect the board to a secure WebSocket endpoint (the ones starting with wss://
)... the tricky part is the secure. All of this using the Arduino programming environment.
... by the way, if you already know this and just want to know how to configure Secure WebSockets, click here.
Setup
OK, so first things first, for this (apart from a ESP8266 board) you'll need to configure your environment. These are some support links before we get into the code.
- Get the Arduino IDE
- Install support for the ESP8266
- Install Markus Sattler's arduinoWebSockets library [on github]. Can be found on the Arduino IDE library manager as "WebSockets" by Markus Sattler.
Base code
OK, let's get to the fun. First we'll write the code to establish and test the WebSocket connection and then we'll move into using secure websockets.
These are the imports that we will need.
1 2 3 4 5 6 |
|
Then let's write the initialization code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
If we send this code to the board and open the Serial Monitor we should get this:
----- LINE NOISE -----
Connectingscandone
....scandone
.scandone
----- THIS IS THE IMPORTANT PART ↓↓↓ -----
connected with <YOUR_WIFI_NAME>, channel 6
dhcp client start...
ip:<YOUR_IP>,mask:255.255.255.0,gw:<YOUR_ROUTER_IP>
Connected, IP address: <YOUR_IP>
This looks alright!
OK, so now let's add some not-yet-secure WebSockets. We'll use ws://echo.websocket.org
(the non ws*s*://
variant) for that. This endpoint will reply echoing any message we send to it.
We'll declare a global WebSocketsClient
object just next to the WiFiMulti
one:
1 2 |
|
Extend the final part of the setup()
to establish the WebSocket connection:
1 2 3 4 5 6 7 8 9 10 11 |
|
Add a webSocketEventHandler
method to handle the WebSocket events:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
And finally, allow the webSocket
to do some stuff on the loop()
method:
1 2 3 |
|
If we set the "Debug Port" (on Arduino IDE Tools
menu) to Serial
and send this to the board, the Serial Monitor should output something like this:
----- Same as before -----
Connected, IP address: <YOUR_IP>
----- Client (board) establishes the HTTP connection -----
[WS-Client] connect ws...
[WS-Client] connected to echo.websocket.org:80.
[WS-Client][sendHeader] sending header...
[WS-Client][sendHeader] handshake GET / HTTP/1.1
Host: echo.websocket.org:80
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: v8umjt63l/fKbljxLZqtOw==
Sec-WebSocket-Protocol: arduino
Origin: file://
User-Agent: arduino-WebSocket-Client
[write] n: 248 t: 4620
[WS-Client][sendHeader] sending header... Done (24475us).
----- Server accepts and upgrades to WebSocket -----
[WS-Client][handleHeader] RX: HTTP/1.1 101 Web Socket Protocol Handshake
[WS-Client][handleHeader] RX: Access-Control-Allow-Credentials: true
[WS-Client][handleHeader] RX: Access-Control-Allow-Headers: content-type
[WS-Client][handleHeader] RX: Access-Control-Allow-Headers: authorization
[WS-Client][handleHeader] RX: Access-Control-Allow-Headers: x-websocket-extensions
[WS-Client][handleHeader] RX: Access-Control-Allow-Headers: x-websocket-version
[WS-Client][handleHeader] RX: Access-Control-Allow-Headers: x-websocket-protocol
[WS-Client][handleHeader] RX: Access-Control-Allow-Origin: null
[WS-Client][handleHeader] RX: Connection: Upgrade
[WS-Client][handleHeader] RX: Date: Sun, 27 Oct 2019 18:51:03 GMT
[WS-Client][handleHeader] RX: Sec-WebSocket-Accept: lJ/edyvQ6g6zrdp3L3YKcwUU+8k=
[WS-Client][handleHeader] RX: Server: Kaazing Gateway
[WS-Client][handleHeader] RX: Upgrade: websocket
[WS-Client][handleHeader] Header read fin.
[WS-Client][handleHeader] Client settings:
[WS-Client][handleHeader] - cURL: /
[WS-Client][handleHeader] - cKey: v8umjt63l/fKbljxLZqtOw==
[WS-Client][handleHeader] Server header:
[WS-Client][handleHeader] - cCode: 101
[WS-Client][handleHeader] - cIsUpgrade: 1
[WS-Client][handleHeader] - cIsWebsocket: 1
[WS-Client][handleHeader] - cAccept: lJ/edyvQ6g6zrdp3L3YKcwUU+8k=
[WS-Client][handleHeader] - cProtocol: arduino
[WS-Client][handleHeader] - cExtensions:
[WS-Client][handleHeader] - cVersion: 0
[WS-Client][handleHeader] - cSessionId:
[WS-Client][handleHeader] Websocket connection init done.
[WS][0][headerDone] Header Handling Done.
----- Connection is complete, the message on connection is sent -----
[WSc] Connected to url: /
[WS][0][sendFrame] ------- send message frame -------
[WS][0][sendFrame] fin: 1 opCode: 1 mask: 1 length: 10 headerToPayload: 0
[WS][0][sendFrame] text: Hi there!
[WS][0][sendFrame] pack to one TCP package...
[write] n: 16 t: 4879
[WS][0][sendFrame] sending Frame Done (4490us).
[WS][0][handleWebsocketWaitFor] size: 2 cWsRXsize: 0
----- A message is received -----
[readCb] n: 2 t: 4981
[WS][0][handleWebsocketWaitFor][readCb] size: 2 ok: 1
[WS][0][handleWebsocket] ------- read massage frame -------
[WS][0][handleWebsocket] fin: 1 rsv1: 0 rsv2: 0 rsv3 0 opCode: 1
[WS][0][handleWebsocket] mask: 0 payloadLen: 10
[readCb] n: 10 t: 4996
[WS][0][handleWebsocket] text: Hi there!
[WSc] get text: Hi there!
Secure WebSockets
So we got ourselves a nice base, now let's add the "secure" part. The "secure" part of WebSockets comes from being wrapped on HTTPS. It's a deep, complex topic, but from the technology point of view it guarantees that we're both talking with the server in an encrypted way, and we're talking with the correct server, not one that intercepted the connection.
Because the underlying technology is the same as a normal web request, usually no especial preparation is needed for secure WebSockets, but in case of Arduino applications, normally they lack the inner storage needed to securely authenticate all existing web servers, so we have to add that information ourselves.
What we need to check that the server we're talking to is the correct one and thus the connection is secure, we need its certificate. So let's start with the incantations... (We're considering you have a Linux machine with the openssl
command )
The only command we have to launch is this:
openssl s_client -showcerts -connect echo.websocket.org:443 </dev/null
This will give us a lot of information, we are interested specifically on the Certificate chain
part:
---
Certificate chain
0 s:CN = websocket.org
i:C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
-----BEGIN CERTIFICATE-----
MIIFYjCCBEqgAwIBAgISA1yNPK8GaSXHX8bHR35U30NiMA0GCSqGSIb3DQEBCwUA
MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
# More seemingly-random certificate data
Z8Evavw4DpmM9eEQtmnuoGIsFQQLxeiFeAOc1FFuFuxUadaCubTz4e4nA5EQzJfc
ZNpyAFO1yXTZKe3860Rn7ayrx+L9m9q5o03dBjW4pDDIFNmPAkA=
-----END CERTIFICATE-----
1 s:C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
i:O = Digital Signature Trust Co., CN = DST Root CA X3
-----BEGIN CERTIFICATE-----
MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
# More seemingly-random certificate data
PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6
KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==
-----END CERTIFICATE-----
---
Server certificate
subject=CN = websocket.org
issuer=C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
---
These are the certificates we talked about, so which one to pick?
To be honest I'm not totally sure, but from testing with some certificates generated from Let's Encrypt the fast answer is: take the last one (so the one finishing with /DNFu0Qg==
). You'd think is not important which one to pick, but ideally you should add the root certificates, as they are the ones that will stay in place for longer. This is even more important if your service uses Let's Encrypt, as the certificates they generate have to be renewed every 3 months (because of their security policy).
Right, so we have out certificate, what now?
Well, not much, let's just add it as a global variable besides the others, with some wrapper:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
And use it to initialize the websocket (not the change in the port from 80
to 443
):
1 2 |
|
Compile and...
----- Same as every WiFi connection -----
Connected, IP address: <YOUR_IP>
----- Certificate configuration, connection establishment -----
[WS-Client] connect wss...
[WS-Client] setting CA certificate[WS-Client] connected to echo.websocket.org:443.
[WS-Client][sendHeader] sending header...
[WS-Client][sendHeader] handshake GET / HTTP/1.1
Host: echo.websocket.org:443
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: tawLOtLL9KAplr+Y67mjcQ==
Sec-WebSocket-Protocol: arduino
Origin: file://
User-Agent: arduino-WebSocket-Client
[write] n: 249 t: 5614
[WS-Client][sendHeader] sending header... Done (139502us).
----- Server accepts and upgrades to WebSocket -----
[WS-Client][handleHeader] RX: HTTP/1.1 101 Web Socket Protocol Handshake
[WS-Client][handleHeader] RX: Access-Control-Allow-Credentials: true
[WS-Client][handleHeader] RX: Access-Control-Allow-Headers: content-type
[WS-Client][handleHeader] RX: Access-Control-Allow-Headers: authorization
[WS-Client][handleHeader] RX: Access-Control-Allow-Headers: x-websocket-extensions
[WS-Client][handleHeader] RX: Access-Control-Allow-Headers: x-websocket-version
[WS-Client][handleHeader] RX: Access-Control-Allow-Headers: x-websocket-protocol
[WS-Client][handleHeader] RX: Access-Control-Allow-Origin: null
[WS-Client][handleHeader] RX: Connection: Upgrade
[WS-Client][handleHeader] RX: Date: Sun, 27 Oct 2019 19:50:41 GMT
[WS-Client][handleHeader] RX: Sec-WebSocket-Accept: avZJ3xQqsxynXMlG+8FLvVAsWGs=
[WS-Client][handleHeader] RX: Server: Kaazing Gateway
[WS-Client][handleHeader] RX: Upgrade: websocket
[WS-Client][handleHeader] Header read fin.
[WS-Client][handleHeader] Client settings:
[WS-Client][handleHeader] - cURL: /
[WS-Client][handleHeader] - cKey: tawLOtLL9KAplr+Y67mjcQ==
[WS-Client][handleHeader] Server header:
[WS-Client][handleHeader] - cCode: 101
[WS-Client][handleHeader] - cIsUpgrade: 1
[WS-Client][handleHeader] - cIsWebsocket: 1
[WS-Client][handleHeader] - cAccept: avZJ3xQqsxynXMlG+8FLvVAsWGs=
[WS-Client][handleHeader] - cProtocol: arduino
[WS-Client][handleHeader] - cExtensions:
[WS-Client][handleHeader] - cVersion: 0
[WS-Client][handleHeader] - cSessionId:
[WS-Client][handleHeader] Websocket connection init done.
[WS][0][headerDone] Header Handling Done.
----- Connection is complete, the message on connection is sent -----
[WSc] Connected to url: /
[WS][0][sendFrame] ------- send message frame -------
[WS][0][sendFrame] fin: 1 opCode: 1 mask: 1 length: 10 headerToPayload: 0
[WS][0][sendFrame] text: Hi there!
[WS][0][sendFrame] pack to one TCP package...
[write] n: 16 t: 5889
[WS][0][sendFrame] sending Frame Done (106589us).
[WS][0][handleWebsocketWaitFor] size: 2 cWsRXsize: 0
----- A message is received -----
[readCb] n: 2 t: 5999
[WS][0][handleWebsocketWaitFor][readCb] size: 2 ok: 1
[WS][0][handleWebsocket] ------- read massage frame -------
[WS][0][handleWebsocket] fin: 1 rsv1: 0 rsv2: 0 rsv3 0 opCode: 1
[WS][0][handleWebsocket] mask: 0 payloadLen: 10
[readCb] n: 10 t: 6016
[WS][0][handleWebsocket] text: Hi there!
[WSc] get text: Hi there!
And that's it, Secure WebSockets with Arduino!
pd: Here is the final code ;)
References
- ESP8266 Pinout reference ← it'll come in handy :)