Saltar ó contido principal

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.

  1. Get the Arduino IDE
  2. Install support for the ESP8266
  3. 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
// WebSocket configuration
#include <WebSocketsClient.h>

// WiFi configuration
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>

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
ESP8266WiFiMulti WiFiMulti;

void setup() {
    // Configure serial output
    Serial.begin(115200);

    // Activate debug output. All help is welcome in this phase.
    Serial.setDebugOutput(true);

    // Connect wifi
    DEBUG_WEBSOCKETS("Connecting");

    // Configure WiFi name and password
    WiFiMulti.addAP("SET_HERE_YOUR_WIFI_NAME", "SET_HERE_YOUR_WIFI_PASSWORD");

    // Wait for the connection to be established
    while(WiFiMulti.run() != WL_CONNECTED) {
        delay(500);
        DEBUG_WEBSOCKETS(".");
    }

    // Show connection data
    Serial.println();
    Serial.print("Connected, IP address: ");
    Serial.println(WiFi.localIP());
}

void loop() {
    // Nothing here _YET_
}

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
ESP8266WiFiMulti WiFiMulti;
WebSocketsClient webSocket;

Extend the final part of the setup() to establish the WebSocket connection:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    // Show connection data
    Serial.println();
    Serial.print("Connected, IP address: ");
    Serial.println(WiFi.localIP());

    // Establish websocket connection
    webSocket.begin("echo.websocket.org", 80, "/");

    // Setup connection handler
    webSocket.onEvent(webSocketEventHandler);
}

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
void webSocketEventHandler(WStype_t type, uint8_t * payload, size_t length) {
    switch(type) {
    case WStype_DISCONNECTED:
        Serial.printf("[WSc] Disconnected\n");
        break;
    case WStype_CONNECTED: // When WebSocket does connect, send a message
        Serial.printf("[WSc] Connected to url: %s\n",  payload);
        webSocket.sendTXT("Hi there!\n");
        break;

    case WStype_TEXT:  // When a message is received, print it
        Serial.printf("[WSc] get text: %s\n", payload);
        break;

    case WStype_BIN:
        Serial.printf("[WSc] get binary length: %u\n", length);
        break;

    case WStype_ERROR:
        // Error
        Serial.printf("[WSc] get error length: %u\n", length);
        break;

    default:
        Serial.printf("[WSc] Unknown transmission: %i. Probably can be ignored.", type);
    }
}

And finally, allow the webSocket to do some stuff on the loop() method:

1
2
3
void loop() {
    webSocket.loop();
}

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
ESP8266WiFiMulti WiFiMulti;
WebSocketsClient webSocket;
const char ENDPOINT_CA_CERT[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow
SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT
GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC
AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF
q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8
SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0
Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA
a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj
/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T
AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG
CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv
bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k
c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw
VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC
ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz
MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu
Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF
AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo
uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/
wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu
X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG
PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6
KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==
-----END CERTIFICATE-----
)EOF";

And use it to initialize the websocket (not the change in the port from 80 to 443):

1
2
// Establish websocket connection
webSocket.beginSslWithCA("echo.websocket.org", 443, "/", ENDPOINT_CA_CERT);

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