gilmaimon/ArduinoWebsockets

Secured-Two-Way: Client certificate is not sent if used along with setTrustAnchors

adelin-mcbsoft opened this issue · 4 comments

Hi @gilmaimon ,

Long time no speak.
I came across a bug of the library, I'm laying out details below.
Hope you can have an eye on it,

Thanks,
Regards,
Adelin

Platform used to test:
ESP8266, SDK 2.7.2 on Arduino IDE

Description:

  • Preface

Consider your SecuredTwoWay-Esp8266-Client example .

Before moving on to the main issue, just want to point that the code misses a time-synchronization code block.
ESP's time has to be updated as to a NTP servers time, otherwise you will get this SSL Error:

BSSL:_wait_for_handshake: failed
BSSL:Couldn't connect. Error = 'Certificate is expired or not yet valid.'

since the library does not know what time currently is, to check if the certificate date is valid.

I'm using this setup (the entire code is below):

const char *ntp1 = "time.windows.com";
const char *ntp2 = "pool.ntp.org";
time_t now;
configTime(2 * 3600, 1, ntp1, ntp2);
    while(now < 2 * 3600) {
      Serial.print(".");
      delay(500);
      now = time(nullptr);
    }
  • The main issue

When trying to connect to a WSS server using setTrustedAnchors to verify the remote server (as it is in your example), the library apparently does not send the client certificate (which is set using setClientRSACert ).

nginx in particular reports this: "client sent no required SSL certificate while reading client request headers".
On the other hand, in its logs I can see that it does make the server SSL handshake successfully and the library does send its headers, but nginx replies with HTTP 400 Bad Request, since it receives no client certificate.

[debug] 7345#0: *221 reusable connection: 1
[debug] 7345#0: *221 epoll add event: fd:3 op:1 ev:80002001
[debug] 7345#0: *221 http check ssl handshake
[debug] 7345#0: *221 http recv(): 1
[debug] 7345#0: *221 https ssl handshake: 0x16
[debug] 7345#0: *221 tcp_nodelay
[debug] 7345#0: *221 reusable connection: 0
[debug] 7345#0: *221 SSL server name: "192.168.0.123"
[debug] 7345#0: *221 SSL_do_handshake: -1
[debug] 7345#0: *221 SSL_get_error: 2
[debug] 7345#0: *221 SSL handshake handler: 0
[debug] 7345#0: *221 SSL_do_handshake: -1
[debug] 7345#0: *221 SSL_get_error: 2
[debug] 7345#0: *221 SSL handshake handler: 0
[debug] 7345#0: *221 SSL_do_handshake: 1
[debug] 7345#0: *221 SSL: TLSv1.2, cipher: "ECDHE-RSA-CHACHA20-POLY1305 TLSv1.2 Kx=ECDH Au=RSA Enc=CHACHA20/POLY1305(256) Mac=AEAD"
[debug] 7345#0: *221 reusable connection: 1
[debug] 7345#0: *221 http wait request handler
[debug] 7345#0: *221 malloc: 00000000012C4780:1024
[debug] 7345#0: *221 SSL_read: -1
[debug] 7345#0: *221 SSL_get_error: 2
[debug] 7345#0: *221 free: 00000000012C4780
[debug] 7345#0: *221 http wait request handler
[debug] 7345#0: *221 malloc: 00000000012C4780:1024
[debug] 7345#0: *221 SSL_read: 243
[debug] 7345#0: *221 SSL_read: -1
[debug] 7345#0: *221 SSL_get_error: 2
[debug] 7345#0: *221 reusable connection: 0
[debug] 7345#0: *221 posix_memalign: 00000000012DB150:4096 @16
[debug] 7345#0: *221 http process request line
[debug] 7345#0: *221 http request line: "GET /ws/ HTTP/1.1"
[debug] 7345#0: *221 http uri: "/ws/"
[debug] 7345#0: *221 http args: ""
[debug] 7345#0: *221 http exten: ""
[debug] 7345#0: *221 posix_memalign: 000000000139B0E0:4096 @16
[debug] 7345#0: *221 http process request header line
[debug] 7345#0: *221 http header: "Host: 192.168.0.123"
[debug] 7345#0: *221 http header: "Sec-WebSocket-Key: MDEyMzQ1Njc4OWFiY2RlZg=="
[debug] 7345#0: *221 http header: "Upgrade: websocket"
[debug] 7345#0: *221 http header: "Connection: Upgrade"
[debug] 7345#0: *221 http header: "Sec-WebSocket-Version: 13"
[debug] 7345#0: *221 http header: "User-Agent: TinyWebsockets Client"
[debug] 7345#0: *221 http header: "Origin: https://github.com/gilmaimon/TinyWebsockets"
[debug] 7345#0: *221 http header done
[info] 7345#0: *221 client sent no required SSL certificate while reading client request headers, client: 192.168.137.1, server: , request: "GET /ws/ HTTP/1.1", host: "192.168.0.123"
[debug] 7345#0: *221 http finalize request: 496, "/ws/?" a:1, c:1
[debug] 7345#0: *221 event timer del: 3: 49296422
[debug] 7345#0: *221 http special response: 496, "/ws/?"
[debug] 7345#0: *221 http set discard body
[debug] 7345#0: *221 HTTP/1.1 400 Bad Request^M
Server: nginx/1.18.0^M
Date: Sun, 19 Jul 2020 05:54:05 GMT^M
Content-Type: text/html^M
Content-Length: 237^M
Connection: close^M

[debug] 7345#0: *221 write new buf t:1 f:0 00000000012DBFE0, pos 00000000012DBFE0, size: 152 file: 0, size: 0
[debug] 7345#0: *221 http write filter: l:0 f:0 s:152
[debug] 7345#0: *221 http output filter "/ws/?"
[debug] 7345#0: *221 http copy filter: "/ws/?"
[debug] 7345#0: *221 http postpone filter "/ws/?" 00000000012DC140
[debug] 7345#0: *221 write old buf t:1 f:0 00000000012DBFE0, pos 00000000012DBFE0, size: 152 file: 0, size: 0
[debug] 7345#0: *221 write new buf t:0 f:0 0000000000000000, pos 00000000006C8A00, size: 184 file: 0, size: 0
[debug] 7345#0: *221 write new buf t:0 f:0 0000000000000000, pos 00000000006C9AE0, size: 53 file: 0, size: 0
[debug] 7345#0: *221 http write filter: l:1 f:0 s:389
[debug] 7345#0: *221 http write filter limit 0
[debug] 7345#0: *221 posix_memalign: 000000000139A160:512 @16
[debug] 7345#0: *221 malloc: 000000000133ED50:16384
[debug] 7345#0: *221 SSL buf copy: 152
[debug] 7345#0: *221 SSL buf copy: 184
[debug] 7345#0: *221 SSL buf copy: 53
[debug] 7345#0: *221 SSL to write: 389
[debug] 7345#0: *221 SSL_write: 389
[debug] 7345#0: *221 http write filter 0000000000000000
[debug] 7345#0: *221 http copy filter: 0 "/ws/?"
[debug] 7345#0: *221 http finalize request: 0, "/ws/?" a:1, c:1
[debug] 7345#0: *221 http request count:1 blk:0
[debug] 7345#0: *221 http close request
[debug] 7345#0: *221 http log handler
[debug] 7345#0: *221 free: 00000000012DB150, unused: 0
[debug] 7345#0: *221 free: 000000000139B0E0, unused: 2815
[debug] 7345#0: *221 close http connection: 3
[debug] 7345#0: *221 SSL_shutdown: 1
[debug] 7345#0: *221 reusable connection: 0

The WebSocket error (generated by the library) is 1002 (an endpoint is terminating the connection due to a protocol error).

Using the client certificates together with setFingerprint instead of setTrustAnchors works without any issue, however setFingerprint has the downside of not being valid after the remote server certificate is renewed. (consider Let's Encrypt for example, which renews it every 3 months).

Using setTrustedAnchors alone (by disabling client certificate requirement both on server and client side) also works.

Since on ESP8266 the extended SSL class is BearSSL, I implemented locally the setKnownKey method, which allows to specify the public key of a certificate (which does not change at each renewal), however, it fails in the same way as above (but works when used alone), which makes me think there is a problem during the handshake computing.

It could also be a bug in WiFiClientSecure as well, however I found no issue with it so far, which makes me think the problem still lies with the WebSockets library.

The fact that it works separately it proves that the certificates are okay and there's no issue with them.
They are RSA 1024bit and the common and subject name are both set on my local testing IP (192.168.0.123).
AFAIK, BearSSL only fails when using certificates lower (or equal to) 512bit.

To Reproduce
Implement a SSL server that asks for client certificates (I used nginx), take the SecuredTwoWay-Esp8266-Client example, add the time-sync code block along with your own certificates, enable debug and check the behavior I described above.

Expected behavior
To have it connected and working while using both client certificate (setClientRSACert) and trusted anchors (setTrustAnchors), in the same fashion it works using it with the setFingerprint method.

Code

#include <ArduinoWebsockets.h>
#include <ESP8266WiFi.h>

const char* ssid = "your ssid"; //Enter SSID
const char* password = "your password"; //Enter Password

const char* websockets_connection_string = "wss://192.168.0.123/"; //Enter server adress

// The hardcoded certificate authority for this example.
// Don't use it on your own apps!!!!!
const char ca_cert[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
[your certificate authority here]
-----END CERTIFICATE-----
)EOF";

// The client's private key which must be kept secret
const char client_private_key[] PROGMEM = R"EOF(
-----BEGIN RSA PRIVATE KEY-----
[your client certificate's private key]
-----END RSA PRIVATE KEY-----
)EOF";

// The clint's public certificate which must be shared
const char client_cert[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
[your client certificate]
-----END CERTIFICATE-----
)EOF";

using namespace websockets;

const char *ntp1 = "time.windows.com";
const char *ntp2 = "pool.ntp.org";
time_t now;

void onMessageCallback(WebsocketsMessage message) {
    Serial.print("Got Message: ");
    Serial.println(message.data());
}

void onEventsCallback(WebsocketsEvent event, String data) {
    if(event == WebsocketsEvent::ConnectionOpened) {
        Serial.println("Connnection Opened");
    } else if(event == WebsocketsEvent::ConnectionClosed) {
        Serial.println("Connnection Closed");
    } else if(event == WebsocketsEvent::GotPing) {
        Serial.println("Got a Ping!");
    } else if(event == WebsocketsEvent::GotPong) {
        Serial.println("Got a Pong!");
    }
}

WebsocketsClient client;
void setup() {
    Serial.begin(115200);
    // Connect to wifi
    WiFi.begin(ssid, password);

    // Wait some time to connect to wifi
    for(int i = 0; i < 10 && WiFi.status() != WL_CONNECTED; i++) {
        Serial.print(".");
        delay(1000);
    }

    configTime(2 * 3600, 1, ntp1, ntp2);
    while(now < 2 * 3600) {
      Serial.print(".");
      delay(500);
      now = time(nullptr);
    }

    // run callback when messages are received
    client.onMessage(onMessageCallback);
    
    // run callback when events are occuring
    client.onEvent(onEventsCallback);

    // Before connecting, set the ssl certificates and key of the server
    X509List cert(ca_cert);
    client.setTrustAnchors(&cert);

    X509List *serverCertList = new X509List(client_cert);
    PrivateKey *serverPrivKey = new PrivateKey(client_private_key);
    client.setClientRSACert(serverCertList, serverPrivKey);
    
    // Connect to server
    client.connect(websockets_connection_string);

    // Send a message
    client.send("Hello Server");

    // Send a ping
    client.ping();
}

void loop() {
    client.poll();
}

Additional context
After this issue is resolved, would be nice to have:

  • A global implementation of the setKnownKey() method .
  • The possibility to specify elliptic curve certificates (ECC) too (instead of just RSA), using setClientECCert() method, since they are more suitable for IoT devices.

Later testing:

I've tested the WiFiClientSecure itself by making a HTTPS request using setTrustedAnchors along with the client certificate (setClientRSACert) and it works without any issue, so definitely there's a bug in the library.

I attach the code I used for making the test.

#include <ESP8266WiFi.h>
#include <WiFiClientSecure.h>

#ifndef STASSID
#define STASSID "Your WiFi Network"
#define STAPSK  "Your WiFi Password"
#endif

const char* ssid = STASSID;
const char* password = STAPSK;

const char* host = "192.168.0.123";
const int httpsPort = 443;

// The CA used to sign the SSL server certificate
const char ca_cert[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
[your own CA Cert]
-----END CERTIFICATE-----
)EOF";

// The client's private key which must be kept secret
const char client_private_key[] PROGMEM = R"EOF(
-----BEGIN RSA PRIVATE KEY-----
[your own private key]
-----END RSA PRIVATE KEY-----
)EOF";

// The client's public certificate which must be shared
const char client_cert[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
[your own certificate]
-----END CERTIFICATE-----
)EOF";


/* NTP Time Servers */
const char *ntp1 = "time.windows.com";
const char *ntp2 = "pool.ntp.org";
time_t now;

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

  Serial.println("Setting time... ");
  
  configTime(2 * 3600, 1, ntp1, ntp2);
  while(now < 2 * 3600) {
    Serial.print(".");
    delay(500);
    now = time(nullptr);
  }
  Serial.println("");
  Serial.println("Time set, connecting to server...");

  // Use WiFiClientSecure class to create TLS connection
  WiFiClientSecure client;
  Serial.print("Connecting to ");
  Serial.println(host);

  // Serial.printf("Using fingerprint '%s'\n", fingerprint);
  // client.setFingerprint(fingerprint);

  X509List *serverTrustedCA = new X509List(ca_cert);
  client.setTrustAnchors(serverTrustedCA);

  X509List *serverCertList = new X509List(client_cert);
  PrivateKey *serverPrivKey = new PrivateKey(client_private_key);
  client.setClientRSACert(serverCertList, serverPrivKey);

  if (!client.connect(host, httpsPort)) {
    Serial.println("Connection failed :(");
    return;
  }

  String url = "/";
  Serial.print("Requesting URL: ");
  Serial.println(url);

  client.print(String("GET ") + url + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" +
               "User-Agent: BuildFailureDetectorESP8266\r\n" +
               "Connection: close\r\n\r\n");

  Serial.println("Request Sent !");
  while (client.connected()) {
    String line = client.readStringUntil('\n');
    Serial.println(line);
    if (line == "\r") {
      Serial.println("Headers received.");
      break;
    }
  }
  Serial.println("Content: ");
  String line = client.readStringUntil('\n');
  Serial.println(line);
}

void loop() {}

And I found the bug and the necessary fix.
Fortunately, it's just a code block swap.

In websockets_client.cpp, between line 199 and 209 you set the proper (optional) certificates, as defined by the user.

However, the _optional_ssl_trust_anchors are the latest to be set using the WiFiClientSecure's client->setTrustAnchors(this->_optional_ssl_trust_anchors); method. And here comes the glich:

When the method is called in WiFiClientSecureBearSSL.h file of the ESP8266's SDK, it executes clearAuthenticationSettings(); which clears all the authentication items previously set, which means whatever client certificates have been set before - will be cleared (and obviously not set).

Fortunately, all we have to do is modify the websockets_client.cpp file and swap the last 2 "if" statements of the mentioned code block, as follows:

            if(this->_optional_ssl_fingerprint) {
                client->setFingerprint(this->_optional_ssl_fingerprint);
            }
            if(this->_optional_ssl_trust_anchors) {
                client->setTrustAnchors(this->_optional_ssl_trust_anchors);
            }
            if(this->_optional_ssl_cert && this->_optional_ssl_private_key) {
                client->setClientRSACert(this->_optional_ssl_cert, this->_optional_ssl_private_key);
            }

This way the client certificate (set using setClientRSACert) will be called last so it will be properly set (and not cleared anymore).

Looking forward for the fix to be included and merged!

All the best,
Adelin

Awesome documentation and work @adelin-mcbsoft , as always!

Hopefully the PR will be merged soon.

Thanks.

Solved by you :p