diff --git a/lib_bearssl/libbearssl.a b/lib_bearssl/libbearssl.a new file mode 100644 index 000000000..dc8a6a34c Binary files /dev/null and b/lib_bearssl/libbearssl.a differ diff --git a/platformio.ini b/platformio.ini index a5c0946d1..252e4b700 100755 --- a/platformio.ini +++ b/platformio.ini @@ -70,14 +70,16 @@ build_flags = ${esp82xx_defaults.build_flags} platform = espressif8266@~2.2.1 build_flags = ${esp82xx_defaults.build_flags} -Wl,-Teagle.flash.1m.ld -; Code optimization see https://github.com/esp8266/Arduino/issues/5790#issuecomment-475672473 +; Code optimization see https://github.com/esp8266/Arduino/issues/5790#issuecomment-475672473 -O2 -DBEARSSL_SSL_BASIC +; link with an memory optimized version of lib_bearssl + -Llib_bearssl -lbearssl ; nonos-sdk 22x -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x ; nonos-sdk-pre-v3 ; -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK3 -; lwIP 1.4 +; lwIP 1.4 ; -DPIO_FRAMEWORK_ARDUINO_LWIP_HIGHER_BANDWIDTH ; lwIP 2 - Low Memory ; -DPIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY @@ -96,13 +98,15 @@ build_flags = ${esp82xx_defaults.build_flags} platform = https://github.com/platformio/platform-espressif8266.git#feature/stage build_flags = ${esp82xx_defaults.build_flags} -Wl,-Teagle.flash.1m.ld -; Code optimization see https://github.com/esp8266/Arduino/issues/5790#issuecomment-475672473 +; Code optimization see https://github.com/esp8266/Arduino/issues/5790#issuecomment-475672473 -O2 -DBEARSSL_SSL_BASIC ; nonos-sdk 22x -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x +; if you compile for AWS IoT, you should set MQTT keep alive to at least 30s instead of 10s, include is required here for PubSubClient lib + -DMQTT_KEEPALIVE=30 ; nonos-sdk-pre-v3 -; -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK3 +; -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK3 ; lwIP 1.4 ; -DPIO_FRAMEWORK_ARDUINO_LWIP_HIGHER_BANDWIDTH ; lwIP 2 - Low Memory diff --git a/sonoff/StackThunk_light.cpp b/sonoff/StackThunk_light.cpp new file mode 100644 index 000000000..1e27888ec --- /dev/null +++ b/sonoff/StackThunk_light.cpp @@ -0,0 +1,138 @@ +/* + StackThunk_light.c - Allow use second stack for BearSSL calls + Light version with reduced Stack size due to Tasmota optimizations. + + BearSSL uses a significant amount of stack space, much larger than + the default Arduino core stack. These routines handle swapping + between a secondary, user-allocated stack on the heap and the real + stack. + + Copyright (c) 2017 Earle F. Philhower, III. All rights reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Modified 8 May 2015 by Hristo Gochkov (proper post and file upload handling) +*/ + +#include +#include +#include "StackThunk_light.h" +#include + +extern "C" { + +uint32_t *stack_thunk_light_ptr = NULL; +uint32_t *stack_thunk_light_top = NULL; +uint32_t *stack_thunk_light_save = NULL; /* Saved A1 while in BearSSL */ +uint32_t stack_thunk_light_refcnt = 0; + +//#define _stackSize (5600/4) +#define _stackSize (5100/4) // using a light version of bearssl we can save 1KB +#define _stackPaint 0xdeadbeef + +/* Add a reference, and allocate the stack if necessary */ +void stack_thunk_light_add_ref() +{ + stack_thunk_light_refcnt++; + if (stack_thunk_light_refcnt == 1) { + stack_thunk_light_ptr = (uint32_t *)malloc(_stackSize * sizeof(uint32_t)); + stack_thunk_light_top = stack_thunk_light_ptr + _stackSize - 1; + stack_thunk_light_save = NULL; + stack_thunk_light_repaint(); + } +} + +/* Drop a reference, and free stack if no more in use */ +void stack_thunk_light_del_ref() +{ + if (stack_thunk_light_refcnt == 0) { + /* Error! */ + return; + } + stack_thunk_light_refcnt--; + if (!stack_thunk_light_refcnt) { + free(stack_thunk_light_ptr); + stack_thunk_light_ptr = NULL; + stack_thunk_light_top = NULL; + stack_thunk_light_save = NULL; + } +} + +void stack_thunk_light_repaint() +{ + if (stack_thunk_light_ptr) { + for (int i=0; i < _stackSize; i++) { + stack_thunk_light_ptr[i] = _stackPaint; + } + } +} + +/* Simple accessor functions used by postmortem */ +uint32_t stack_thunk_light_get_refcnt() { + return stack_thunk_light_refcnt; +} + +uint32_t stack_thunk_light_get_stack_top() { + return (uint32_t)stack_thunk_light_top; +} + +uint32_t stack_thunk_light_get_stack_bot() { + return (uint32_t)stack_thunk_light_ptr; +} + +uint32_t stack_thunk_light_get_cont_sp() { + return (uint32_t)stack_thunk_light_save; +} + +/* Return the number of bytes ever used since the stack was created */ +uint32_t stack_thunk_light_get_max_usage() +{ + uint32_t cnt = 0; + + /* No stack == no usage by definition! */ + if (!stack_thunk_light_ptr) { + return 0; + } + + for (cnt=0; (cnt < _stackSize) && (stack_thunk_light_ptr[cnt] == _stackPaint); cnt++) { + /* Noop, all work done in for() */ + } + return 4 * (_stackSize - cnt); +} + +/* Print the stack from the first used 16-byte chunk to the top, decodable by the exception decoder */ +void stack_thunk_light_dump_stack() +{ + uint32_t *pos = stack_thunk_light_top; + while (pos < stack_thunk_light_ptr) { + if ((pos[0] != _stackPaint) || (pos[1] != _stackPaint) || (pos[2] != _stackPaint) || (pos[3] != _stackPaint)) + break; + pos += 4; + } + ets_printf(">>>stack>>>\n"); + while (pos < stack_thunk_light_ptr) { + ets_printf("%08x: %08x %08x %08x %08x\n", (int32_t)pos, pos[0], pos[1], pos[2], pos[3]); + pos += 4; + } + ets_printf("<< +#include +#include + +extern "C" { +#include "osapi.h" +#include "ets_sys.h" +} +#include "debug.h" +#include "ESP8266WiFi.h" +#include "WiFiClient.h" +#include "WiFiClientSecureLightBearSSL.h" +#include "StackThunk_light.h" +#include "lwip/opt.h" +#include "lwip/ip.h" +#include "lwip/tcp.h" +#include "lwip/inet.h" +#include "lwip/netif.h" +#include +#include "c_types.h" +#include "coredecls.h" + +#define SKEY_ON_STACK // copy private key+cert on stack rather than on heap, this works for now because it takes ~800 bytes +//#define DEBUG_TLS + +#ifdef DEBUG_TLS +#define LOG_HEAP_SIZE(a) _Log_heap_size(a) +void _Log_heap_size(const char *msg) { + register uint32_t *sp asm("a1"); + int freestack = 4 * (sp - g_pcont->stack); + Serial.printf("%s %d, Fragmentation=%d, Thunkstack=%d, Free stack=%d, FreeContStack=%d\n", + msg, ESP.getFreeHeap(), ESP.getHeapFragmentation(), stack_thunk_light_get_max_usage(), + freestack, ESP.getFreeContStack()); +} +#else +#define LOG_HEAP_SIZE(a) +#endif + +// Stack thunked versions of calls +// Initially in BearSSLHelpers.h +extern "C" { +extern unsigned char *thunk_light_br_ssl_engine_recvapp_buf( const br_ssl_engine_context *cc, size_t *len); +extern void thunk_light_br_ssl_engine_recvapp_ack(br_ssl_engine_context *cc, size_t len); +extern unsigned char *thunk_light_br_ssl_engine_recvrec_buf( const br_ssl_engine_context *cc, size_t *len); +extern void thunk_light_br_ssl_engine_recvrec_ack(br_ssl_engine_context *cc, size_t len); +extern unsigned char *thunk_light_br_ssl_engine_sendapp_buf( const br_ssl_engine_context *cc, size_t *len); +extern void thunk_light_br_ssl_engine_sendapp_ack(br_ssl_engine_context *cc, size_t len); +extern unsigned char *thunk_light_br_ssl_engine_sendrec_buf( const br_ssl_engine_context *cc, size_t *len); +extern void thunk_light_br_ssl_engine_sendrec_ack(br_ssl_engine_context *cc, size_t len); +}; + +// Second stack thunked helpers +make_stack_thunk_light(br_ssl_engine_recvapp_ack); +make_stack_thunk_light(br_ssl_engine_recvapp_buf); +make_stack_thunk_light(br_ssl_engine_recvrec_ack); +make_stack_thunk_light(br_ssl_engine_recvrec_buf); +make_stack_thunk_light(br_ssl_engine_sendapp_ack); +make_stack_thunk_light(br_ssl_engine_sendapp_buf); +make_stack_thunk_light(br_ssl_engine_sendrec_ack); +make_stack_thunk_light(br_ssl_engine_sendrec_buf); + +// create new version of Thunk function to store on SYS stack +// unless the Thunk was initialized. Thanks to AES128 GCM, we can keep +// symetric processing on the stack +void min_br_ssl_engine_recvapp_ack(br_ssl_engine_context *cc, size_t len) { + if (stack_thunk_light_get_refcnt()) { + return thunk_light_br_ssl_engine_recvapp_ack(cc, len); + } else { + return br_ssl_engine_recvapp_ack(cc, len); + } +} +unsigned char *min_br_ssl_engine_recvapp_buf(const br_ssl_engine_context *cc, size_t *len) { + if (stack_thunk_light_get_refcnt()) { + return thunk_light_br_ssl_engine_recvapp_buf(cc, len); + } else { + return br_ssl_engine_recvapp_buf(cc, len); + } +} +void min_br_ssl_engine_recvrec_ack(br_ssl_engine_context *cc, size_t len) { + if (stack_thunk_light_get_refcnt()) { + return thunk_light_br_ssl_engine_recvrec_ack(cc, len); + } else { + return br_ssl_engine_recvrec_ack(cc, len); + } +} +unsigned char *min_br_ssl_engine_recvrec_buf(const br_ssl_engine_context *cc, size_t *len) { + if (stack_thunk_light_get_refcnt()) { + return thunk_light_br_ssl_engine_recvrec_buf(cc, len); + } else { + return br_ssl_engine_recvrec_buf(cc, len); + } +} +void min_br_ssl_engine_sendapp_ack(br_ssl_engine_context *cc, size_t len) { + if (stack_thunk_light_get_refcnt()) { + return thunk_light_br_ssl_engine_sendapp_ack(cc, len); + } else { + return br_ssl_engine_sendapp_ack(cc, len); + } +} +unsigned char *min_br_ssl_engine_sendapp_buf(const br_ssl_engine_context *cc, size_t *len) { + if (stack_thunk_light_get_refcnt()) { + return thunk_light_br_ssl_engine_sendapp_buf(cc, len); + } else { + return br_ssl_engine_sendapp_buf(cc, len); + } +} +void min_br_ssl_engine_sendrec_ack(br_ssl_engine_context *cc, size_t len) { + if (stack_thunk_light_get_refcnt()) { + return thunk_light_br_ssl_engine_sendrec_ack(cc, len); + } else { + return br_ssl_engine_sendrec_ack(cc, len); + } +} +unsigned char *min_br_ssl_engine_sendrec_buf(const br_ssl_engine_context *cc, size_t *len) { + if (stack_thunk_light_get_refcnt()) { + return thunk_light_br_ssl_engine_sendrec_buf(cc, len); + } else { + return br_ssl_engine_sendrec_buf(cc, len); + } +} + +// Use min_ instead of original thunk_ +#define br_ssl_engine_recvapp_ack min_br_ssl_engine_recvapp_ack +#define br_ssl_engine_recvapp_buf min_br_ssl_engine_recvapp_buf +#define br_ssl_engine_recvrec_ack min_br_ssl_engine_recvrec_ack +#define br_ssl_engine_recvrec_buf min_br_ssl_engine_recvrec_buf +#define br_ssl_engine_sendapp_ack min_br_ssl_engine_sendapp_ack +#define br_ssl_engine_sendapp_buf min_br_ssl_engine_sendapp_buf +#define br_ssl_engine_sendrec_ack min_br_ssl_engine_sendrec_ack +#define br_ssl_engine_sendrec_buf min_br_ssl_engine_sendrec_buf + +//#define DEBUG_ESP_SSL +#ifdef DEBUG_ESP_SSL +#define DEBUG_BSSL(fmt, ...) DEBUG_ESP_PORT.printf_P((PGM_P)PSTR( "BSSL:" fmt), ## __VA_ARGS__) +//#define DEBUG_BSSL(fmt, ...) Serial.printf(fmt, ## __VA_ARGS__) +#else +#define DEBUG_BSSL(...) +#endif + +namespace BearSSL { + +void WiFiClientSecure_light::_clear() { + // TLS handshake may take more than the 5 second default timeout + _timeout = 10000; // 10 seconds max, it should never go over 6 seconds + + _sc = nullptr; + _ctx_present = false; + _eng = nullptr; + _iobuf_in = nullptr; + _iobuf_out = nullptr; + _now = 0; // You can override or ensure time() is correct w/configTime + setBufferSizes(1024, 1024); // reasonable minimum + _handshake_done = false; + _last_error = 0; + _recvapp_buf = nullptr; + _recvapp_len = 0; + _fingerprint_any = true; // by default accept all fingerprints + _fingerprint1 = nullptr; + _fingerprint2 = nullptr; +} + +// Constructor +WiFiClientSecure_light::WiFiClientSecure_light(int recv, int xmit) : WiFiClient() { + _clear(); +LOG_HEAP_SIZE("StackThunk before"); + //stack_thunk_light_add_ref(); +LOG_HEAP_SIZE("StackThunk after"); + // now finish the setup + setBufferSizes(recv, xmit); // reasonable minimum + allocateBuffers(); +} + +WiFiClientSecure_light::~WiFiClientSecure_light() { + if (_client) { + _client->unref(); + _client = nullptr; + } + //_cipher_list = nullptr; // std::shared will free if last reference + _freeSSL(); +} + +void WiFiClientSecure_light::allocateBuffers(void) { + // We prefer to allocate all buffers at start, rather than lazy allocation and deallocation + // in the long run it avoids heap fragmentation and improves stability + LOG_HEAP_SIZE("allocateBuffers before"); + _sc = std::make_shared(); + LOG_HEAP_SIZE("allocateBuffers ClientContext"); + _iobuf_in = std::shared_ptr(new unsigned char[_iobuf_in_size], std::default_delete()); + _iobuf_out = std::shared_ptr(new unsigned char[_iobuf_out_size], std::default_delete()); + LOG_HEAP_SIZE("allocateBuffers after"); +} + +void WiFiClientSecure_light::setClientECCert(const br_x509_certificate *cert, const br_ec_private_key *sk, + unsigned allowed_usages, unsigned cert_issuer_key_type) { + _chain_P = cert; + _chain.data_len = _chain_P->data_len; + _chain.data = nullptr; + _sk_ec_P = sk; + _sk_ec.curve = _sk_ec_P->curve; + _sk_ec.xlen = _sk_ec_P->xlen; + _sk_ec.x = nullptr; + _allowed_usages = allowed_usages; + _cert_issuer_key_type = cert_issuer_key_type; +} + +void WiFiClientSecure_light::setBufferSizes(int recv, int xmit) { + // Following constants taken from bearssl/src/ssl/ssl_engine.c (not exported unfortunately) + const int MAX_OUT_OVERHEAD = 85; + const int MAX_IN_OVERHEAD = 325; + + // The data buffers must be between 512B and 16KB + recv = std::max(512, std::min(16384, recv)); + xmit = std::max(512, std::min(16384, xmit)); + + // Add in overhead for SSL protocol + recv += MAX_IN_OVERHEAD; + xmit += MAX_OUT_OVERHEAD; + _iobuf_in_size = recv; + _iobuf_out_size = xmit; +} + +bool WiFiClientSecure_light::stop(unsigned int maxWaitMs) { + bool ret = WiFiClient::stop(maxWaitMs); // calls our virtual flush() + _freeSSL(); + return ret; +} + +bool WiFiClientSecure_light::flush(unsigned int maxWaitMs) { + (void) _run_until(BR_SSL_SENDAPP); + return WiFiClient::flush(maxWaitMs); +} + +int WiFiClientSecure_light::connect(IPAddress ip, uint16_t port) { + clearLastError(); + if (!WiFiClient::connect(ip, port)) { + setLastError(ERR_TCP_CONNECT); + return 0; + } + return _connectSSL(nullptr); +} + +int WiFiClientSecure_light::connect(const char* name, uint16_t port) { + IPAddress remote_addr; + clearLastError(); + if (!WiFi.hostByName(name, remote_addr)) { + DEBUG_BSSL("connect: Name loopup failure\n"); + setLastError(ERR_CANT_RESOLVE_IP); + return 0; + } + if (!WiFiClient::connect(remote_addr, port)) { + DEBUG_BSSL("connect: Unable to connect TCP socket\n"); + _last_error = ERR_TCP_CONNECT; + return 0; + } + LOG_HEAP_SIZE("Before calling _connectSSL"); + return _connectSSL(name); +} + +int WiFiClientSecure_light::connect(const String& host, uint16_t port) { + return connect(host.c_str(), port); +} + +void WiFiClientSecure_light::_freeSSL() { + _ctx_present = false; + _recvapp_buf = nullptr; + _recvapp_len = 0; + // This connection is toast + _handshake_done = false; +} + +bool WiFiClientSecure_light::_clientConnected() { + return (_client && _client->state() == ESTABLISHED); +} + +uint8_t WiFiClientSecure_light::connected() { + if (available() || (_clientConnected() && _handshake_done)) { + return true; + } + return false; +} + +size_t WiFiClientSecure_light::_write(const uint8_t *buf, size_t size, bool pmem) { + size_t sent_bytes = 0; + + if (!connected() || !size || !_handshake_done) { + return 0; + } + + do { + // Ensure we yield if we need multiple fragments to avoid WDT + if (sent_bytes) { + optimistic_yield(1000); + } + + // Get BearSSL to a state where we can send + if (_run_until(BR_SSL_SENDAPP) < 0) { + break; + } + + if (br_ssl_engine_current_state(_eng) & BR_SSL_SENDAPP) { + size_t sendapp_len; + unsigned char *sendapp_buf = br_ssl_engine_sendapp_buf(_eng, &sendapp_len); + int to_send = size > sendapp_len ? sendapp_len : size; + if (pmem) { + memcpy_P(sendapp_buf, buf, to_send); + } else { + memcpy(sendapp_buf, buf, to_send); + } + br_ssl_engine_sendapp_ack(_eng, to_send); + br_ssl_engine_flush(_eng, 0); + flush(); + buf += to_send; + sent_bytes += to_send; + size -= to_send; + } else { + break; + } + } while (size); + + LOG_HEAP_SIZE("_write"); + return sent_bytes; +} + +size_t WiFiClientSecure_light::write(const uint8_t *buf, size_t size) { + return _write(buf, size, false); +} + +size_t WiFiClientSecure_light::write_P(PGM_P buf, size_t size) { + return _write((const uint8_t *)buf, size, true); +} + +// We have to manually read and send individual chunks. +size_t WiFiClientSecure_light::write(Stream& stream) { + size_t totalSent = 0; + size_t countRead; + size_t countSent; + + if (!connected() || !_handshake_done) { + DEBUG_BSSL("write: Connect/handshake not completed yet\n"); + return 0; + } + + do { + uint8_t temp[256]; // Temporary chunk size same as ClientContext + countSent = 0; + countRead = stream.readBytes(temp, sizeof(temp)); + if (countRead) { + countSent = _write((const uint8_t*)temp, countRead, true); + totalSent += countSent; + } + yield(); // Feed the WDT + } while ((countSent == countRead) && (countSent > 0)); + return totalSent; +} + +int WiFiClientSecure_light::read(uint8_t *buf, size_t size) { + if (!ctx_present() || !_handshake_done) { + return -1; + } + + int avail = available(); + bool conn = connected(); + if (!avail && conn) { + return 0; // We're still connected, but nothing to read + } + if (!avail && !conn) { + DEBUG_BSSL("read: Not connected, none left available\n"); + return -1; + } + + if (avail) { + // Take data from the recvapp buffer + int to_copy = _recvapp_len < size ? _recvapp_len : size; + memcpy(buf, _recvapp_buf, to_copy); + br_ssl_engine_recvapp_ack(_eng, to_copy); + _recvapp_buf = nullptr; + _recvapp_len = 0; + return to_copy; + } + + if (!conn) { + DEBUG_BSSL("read: Not connected\n"); + return -1; + } + return 0; // If we're connected, no error but no read. +} + +int WiFiClientSecure_light::read() { + uint8_t c; + if (1 == read(&c, 1)) { + return c; + } + DEBUG_BSSL("read: failed\n"); + return -1; +} + +int WiFiClientSecure_light::available() { + if (_recvapp_buf) { + return _recvapp_len; // Anything from last call? + } + _recvapp_buf = nullptr; + _recvapp_len = 0; + if (!ctx_present() || _run_until(BR_SSL_RECVAPP, false) < 0) { + return 0; + } + int st = br_ssl_engine_current_state(_eng); + if (st == BR_SSL_CLOSED) { + return 0; // Nothing leftover, SSL is closed + } + if (st & BR_SSL_RECVAPP) { + _recvapp_buf = br_ssl_engine_recvapp_buf(_eng, &_recvapp_len); + return _recvapp_len; + } + + return 0; +} + +int WiFiClientSecure_light::peek() { + if (!ctx_present() || !available()) { + DEBUG_BSSL("peek: Not connected, none left available\n"); + return -1; + } + if (_recvapp_buf && _recvapp_len) { + return _recvapp_buf[0]; + } + DEBUG_BSSL("peek: No data left\n"); + return -1; +} + +size_t WiFiClientSecure_light::peekBytes(uint8_t *buffer, size_t length) { + size_t to_copy = 0; + if (!ctx_present()) { + DEBUG_BSSL("peekBytes: Not connected\n"); + return 0; + } + + _startMillis = millis(); + while ((available() < (int) length) && ((millis() - _startMillis) < 5000)) { + yield(); + } + + to_copy = _recvapp_len < length ? _recvapp_len : length; + memcpy(buffer, _recvapp_buf, to_copy); + return to_copy; +} + +/* --- Copied almost verbatim from BEARSSL SSL_IO.C --- + Run the engine, until the specified target state is achieved, or + an error occurs. The target state is SENDAPP, RECVAPP, or the + combination of both (the combination matches either). When a match is + achieved, this function returns 0. On error, it returns -1. +*/ +int WiFiClientSecure_light::_run_until(unsigned target, bool blocking) { +//LOG_HEAP_SIZE("_run_until 1"); + if (!ctx_present()) { + DEBUG_BSSL("_run_until: Not connected\n"); + return -1; + } + for (int no_work = 0; blocking || no_work < 2;) { + if (blocking) { + // Only for blocking operations can we afford to yield() + optimistic_yield(100); + } + + int state; + state = br_ssl_engine_current_state(_eng); + if (state & BR_SSL_CLOSED) { + return -1; + } + + if (!(_client->state() == ESTABLISHED) && !WiFiClient::available()) { + return (state & target) ? 0 : -1; + } + + /* + If there is some record data to send, do it. This takes + precedence over everything else. + */ + if (state & BR_SSL_SENDREC) { + unsigned char *buf; + size_t len; + int wlen; + + buf = br_ssl_engine_sendrec_buf(_eng, &len); + wlen = WiFiClient::write(buf, len); + if (wlen <= 0) { + /* + If we received a close_notify and we + still send something, then we have our + own response close_notify to send, and + the peer is allowed by RFC 5246 not to + wait for it. + */ + return -1; + } + if (wlen > 0) { + br_ssl_engine_sendrec_ack(_eng, wlen); + } + no_work = 0; + continue; + } + + /* + If we reached our target, then we are finished. + */ + if (state & target) { + return 0; + } + /* + If some application data must be read, and we did not + exit, then this means that we are trying to write data, + and that's not possible until the application data is + read. This may happen if using a shared in/out buffer, + and the underlying protocol is not strictly half-duplex. + This is unrecoverable here, so we report an error. + */ + if (state & BR_SSL_RECVAPP) { + DEBUG_BSSL("_run_until: Fatal protocol state\n"); + return -1; + } + /* + If we reached that point, then either we are trying + to read data and there is some, or the engine is stuck + until a new record is obtained. + */ + if (state & BR_SSL_RECVREC) { + if (WiFiClient::available()) { + unsigned char *buf; + size_t len; + int rlen; + + buf = br_ssl_engine_recvrec_buf(_eng, &len); + rlen = WiFiClient::read(buf, len); + if (rlen < 0) { + return -1; + } + if (rlen > 0) { + br_ssl_engine_recvrec_ack(_eng, rlen); + } + no_work = 0; + continue; + } + } + /* + We can reach that point if the target RECVAPP, and + the state contains SENDAPP only. This may happen with + a shared in/out buffer. In that case, we must flush + the buffered data to "make room" for a new incoming + record. + */ + br_ssl_engine_flush(_eng, 0); + + no_work++; // We didn't actually advance here + } + // We only get here if we ran through the loop without getting anything done + return -1; +} + +bool WiFiClientSecure_light::_wait_for_handshake() { + _handshake_done = false; + while (!_handshake_done && _clientConnected()) { + int ret = _run_until(BR_SSL_SENDAPP); + if (ret < 0) { + DEBUG_BSSL("_wait_for_handshake: failed\n"); + break; + } + if (br_ssl_engine_current_state(_eng) & BR_SSL_SENDAPP) { + _handshake_done = true; + } + optimistic_yield(1000); + } + return _handshake_done; +} + +static uint8_t htoi (unsigned char c) +{ + if (c>='0' && c <='9') return c - '0'; + else if (c>='A' && c<='F') return 10 + c - 'A'; + else if (c>='a' && c<='f') return 10 + c - 'a'; + else return 255; +} + +extern "C" { + + // see https://stackoverflow.com/questions/6357031/how-do-you-convert-a-byte-array-to-a-hexadecimal-string-in-c + void tohex(unsigned char * in, size_t insz, char * out, size_t outsz) { + unsigned char * pin = in; + static const char * hex = "0123456789ABCDEF"; + char * pout = out; + for(; pin < in+insz; pout +=3, pin++){ + pout[0] = hex[(*pin>>4) & 0xF]; + pout[1] = hex[ *pin & 0xF]; + pout[2] = ':'; + if (pout + 3 - out > outsz){ + /* Better to truncate output string than overflow buffer */ + /* it would be still better to either return a status */ + /* or ensure the target buffer is large enough and it never happen */ + break; + } + } + pout[-1] = 0; + } + + + // BearSSL doesn't define a true insecure decoder, so we make one ourselves + // from the simple parser. It generates the issuer and subject hashes and + // the SHA1 fingerprint, only one (or none!) of which will be used to + // "verify" the certificate. + + // Private x509 decoder state + struct br_x509_pubkeyfingerprint_context { + const br_x509_class *vtable; + bool done_cert; // did we parse the first cert already? + bool fingerprint_all; + uint8_t *pubkey_recv_fingerprint; + const uint8_t *fingerprint1; + const uint8_t *fingerprint2; + unsigned usages; // pubkey usage + br_x509_decoder_context ctx; // defined in BearSSL + }; + + // Callback on the first byte of any certificate + static void pubkeyfingerprint_start_chain(const br_x509_class **ctx, const char *server_name) { + br_x509_pubkeyfingerprint_context *xc = (br_x509_pubkeyfingerprint_context *)ctx; + // Don't process anything but the first certificate in the chain + if (!xc->done_cert) { + br_x509_decoder_init(&xc->ctx, nullptr, nullptr, nullptr, nullptr); + } + (void)server_name; // ignore server name + } + + // Callback for each certificate present in the chain (but only operates + // on the first one by design). + static void pubkeyfingerprint_start_cert(const br_x509_class **ctx, uint32_t length) { + (void) ctx; // do nothing + (void) length; + } + + // Callback for each byte stream in the chain. Only process first cert. + static void pubkeyfingerprint_append(const br_x509_class **ctx, const unsigned char *buf, size_t len) { + br_x509_pubkeyfingerprint_context *xc = (br_x509_pubkeyfingerprint_context *)ctx; + // Don't process anything but the first certificate in the chain + if (!xc->done_cert) { + br_x509_decoder_push(&xc->ctx, (const void*)buf, len); + } + } + + // Callback on individual cert end. + static void pubkeyfingerprint_end_cert(const br_x509_class **ctx) { + br_x509_pubkeyfingerprint_context *xc = (br_x509_pubkeyfingerprint_context *)ctx; + xc->done_cert = true; // first cert already processed + } + + static void pubkeyfingerprint_pubkey_fingerprint(br_sha1_context *shactx, br_rsa_public_key rsakey) { + br_sha1_init(shactx); + br_sha1_update(shactx, "ssh-rsa", 7); // tag + br_sha1_update(shactx, rsakey.e, rsakey.elen); // exponent + br_sha1_update(shactx, rsakey.n, rsakey.nlen); // modulus + } + + // Callback when complete chain has been parsed. + // Return 0 on validation success, !0 on validation error + static unsigned pubkeyfingerprint_end_chain(const br_x509_class **ctx) { + br_x509_pubkeyfingerprint_context *xc = (br_x509_pubkeyfingerprint_context *)ctx; + + br_sha1_context sha1_context; + pubkeyfingerprint_pubkey_fingerprint(&sha1_context, xc->ctx.pkey.key.rsa); + br_sha1_out(&sha1_context, xc->pubkey_recv_fingerprint); // copy to fingerprint + + if (!xc->fingerprint_all) { + if (0 == memcmp(xc->fingerprint1, xc->pubkey_recv_fingerprint, 20)) { + return 0; + } + if (0 == memcmp(xc->fingerprint2, xc->pubkey_recv_fingerprint, 20)) { + return 0; + } + return 1; // no match, error + } else { + // Default (no validation at all) or no errors in prior checks = success. + return 0; + } + } + + // Return the public key from the validator (set by x509_minimal) + static const br_x509_pkey *pubkeyfingerprint_get_pkey(const br_x509_class *const *ctx, unsigned *usages) { + const br_x509_pubkeyfingerprint_context *xc = (const br_x509_pubkeyfingerprint_context *)ctx; + + if (usages != NULL) { + *usages = BR_KEYTYPE_KEYX | BR_KEYTYPE_SIGN; // I said we were insecure! + } + return &xc->ctx.pkey; + } + + // Set up the x509 insecure data structures for BearSSL core to use. + void br_x509_pubkeyfingerprint_init(br_x509_pubkeyfingerprint_context *ctx, + const uint8_t *fingerprint1, const uint8_t *fingerprint2, + uint8_t *recv_fingerprint, + bool fingerprint_all) { + static const br_x509_class br_x509_pubkeyfingerprint_vtable PROGMEM = { + sizeof(br_x509_pubkeyfingerprint_context), + pubkeyfingerprint_start_chain, + pubkeyfingerprint_start_cert, + pubkeyfingerprint_append, + pubkeyfingerprint_end_cert, + pubkeyfingerprint_end_chain, + pubkeyfingerprint_get_pkey + }; + + memset(ctx, 0, sizeof * ctx); + ctx->vtable = &br_x509_pubkeyfingerprint_vtable; + ctx->done_cert = false; + ctx->fingerprint1 = fingerprint1; + ctx->fingerprint2 = fingerprint2; + ctx->pubkey_recv_fingerprint = recv_fingerprint; + ctx->fingerprint_all = fingerprint_all; + } + + // We limit to a single cipher to reduce footprint + // we reference it, don't put in PROGMEM + static const uint16_t suites[] = { + BR_TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 + }; + + // Default initializion for our SSL clients + static void br_ssl_client_base_init(br_ssl_client_context *cc) { + br_ssl_client_zero(cc); + // forbid SSL renegociation, as we free the Private Key after handshake + br_ssl_engine_add_flags(&cc->eng, BR_OPT_NO_RENEGOTIATION); + + br_ssl_engine_set_versions(&cc->eng, BR_TLS12, BR_TLS12); + br_ssl_engine_set_suites(&cc->eng, suites, (sizeof suites) / (sizeof suites[0])); + br_ssl_client_set_default_rsapub(cc); + br_ssl_engine_set_default_rsavrfy(&cc->eng); + + // install hashes + br_ssl_engine_set_hash(&cc->eng, br_sha256_ID, &br_sha256_vtable); + br_ssl_engine_set_prf_sha256(&cc->eng, &br_tls12_sha256_prf); + + // AES CTR/GCM small version, not contstant time (we don't really care here as there is no TPM anyways) + br_ssl_engine_set_gcm(&cc->eng, &br_sslrec_in_gcm_vtable, &br_sslrec_out_gcm_vtable); + br_ssl_engine_set_aes_ctr(&cc->eng, &br_aes_small_ctr_vtable); + br_ssl_engine_set_ghash(&cc->eng, &br_ghash_ctmul32); + + // we support only P256 EC curve + br_ssl_engine_set_ec(&cc->eng, &br_ec_p256_m15); + } +} + +// Called by connect() to do the actual SSL setup and handshake. +// Returns if the SSL handshake succeeded. +bool WiFiClientSecure_light::_connectSSL(const char* hostName) { + br_ec_private_key sk_ec; + br_x509_certificate chain; +#ifdef SKEY_ON_STACK + unsigned char chain_data[_chain_P->data_len]; + unsigned char sk_data[_sk_ec_P->xlen]; +#endif + br_x509_pubkeyfingerprint_context *x509_insecure; + + LOG_HEAP_SIZE("_connectSSL.start"); + + stack_thunk_light_add_ref(); + LOG_HEAP_SIZE("Thunk allocated"); + DEBUG_BSSL("_connectSSL: start connection\n"); + _freeSSL(); + clearLastError(); + + _ctx_present = true; + _eng = &_sc->eng; // Allocation/deallocation taken care of by the _sc shared_ptr + + br_ssl_client_base_init(_sc.get()); + + LOG_HEAP_SIZE("_connectSSL before DecoderContext allocation"); + // Only failure possible in the installation is OOM + x509_insecure = (br_x509_pubkeyfingerprint_context*) malloc(sizeof(br_x509_pubkeyfingerprint_context)); + + br_x509_pubkeyfingerprint_init(x509_insecure, _fingerprint1, _fingerprint2, + _recv_fingerprint, _fingerprint_any); + br_ssl_engine_set_x509(_eng, &x509_insecure->vtable); + + br_ssl_engine_set_buffers_bidi(_eng, _iobuf_in.get(), _iobuf_in_size, _iobuf_out.get(), _iobuf_out_size); + + LOG_HEAP_SIZE("_connectSSL after PrivKey allocation"); + // allocate Private key and client certificate + //chain = new X509List(_chain_PEM); + //sk = new PrivateKey(_sk_PEM); + chain.data_len = _chain_P->data_len; +#ifdef SKEY_ON_STACK // allocate on stack + chain.data = &chain_data[0]; +#else // allocate with malloc + chain.data = (unsigned char *) malloc(chain.data_len); +#endif + if (chain.data) memcpy_P(chain.data, _chain_P->data, chain.data_len); + + sk_ec.curve = _sk_ec_P->curve; + sk_ec.xlen = _sk_ec_P->xlen; +#ifdef SKEY_ON_STACK + sk_ec.x = &sk_data[0]; +#else + sk_ec.x = (unsigned char *) malloc(sk_ec.xlen); +#endif + if (sk_ec.x) memcpy_P(sk_ec.x, _sk_ec_P->x, sk_ec.xlen); + LOG_HEAP_SIZE("_connectSSL after PrivKey allocation"); + + // check if memory was correctly allocated + if ((!stack_thunk_light_get_stack_bot()) || (!x509_insecure) || + (!chain.data) || (!sk_ec.x)) { + // memory allocation problem + setLastError(ERR_OOM); +#ifndef SKEY_ON_STACK + free(chain.data); + free(sk_ec.x); +#endif + free(x509_insecure); + stack_thunk_light_del_ref(); + DEBUG_BSSL("_connectSSL: Out of memory\n"); + return false; + } + + // limited to P256 curve + br_ssl_client_set_single_ec(_sc.get(), &chain, 1, + &sk_ec, _allowed_usages, + _cert_issuer_key_type, &br_ec_p256_m15, br_ecdsa_sign_asn1_get_default()); + + if (!br_ssl_client_reset(_sc.get(), hostName, 0)) { +#ifndef SKEY_ON_STACK + free(chain.data); + free(sk_ec.x); +#endif + free(x509_insecure); + stack_thunk_light_del_ref(); + _freeSSL(); + DEBUG_BSSL("_connectSSL: Can't reset client\n"); + return false; + } + + auto ret = _wait_for_handshake(); +#ifdef DEBUG_ESP_SSL + if (!ret) { + DEBUG_BSSL("Couldn't connect. Error = %d\n", getLastError()); + } else { + DEBUG_BSSL("Connected! MFLNStatus = %d\n", getMFLNStatus()); + } +#endif + LOG_HEAP_SIZE("_connectSSL.end"); + stack_thunk_light_del_ref(); + //stack_thunk_light_repaint(); + LOG_HEAP_SIZE("_connectSSL.end, freeing StackThunk"); +#ifndef SKEY_ON_STACK + free(chain.data); + free(sk_ec.x); +#endif + free(x509_insecure); + LOG_HEAP_SIZE("_connectSSL after release of Priv Key"); + return ret; +} + +}; diff --git a/sonoff/WiFiClientSecureLightBearSSL.h b/sonoff/WiFiClientSecureLightBearSSL.h new file mode 100644 index 000000000..719339310 --- /dev/null +++ b/sonoff/WiFiClientSecureLightBearSSL.h @@ -0,0 +1,148 @@ +/* + WiFiClientBearSSL- SSL client/server for esp8266 using BearSSL libraries + - Mostly compatible with Arduino WiFi shield library and standard + WiFiClient/ServerSecure (except for certificate handling). + + Copyright (c) 2018 Earle F. Philhower, III + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + + +#ifndef wificlientlightbearssl_h +#define wificlientlightbearssl_h +#include +#include "WiFiClient.h" +#include +#include "BearSSLHelpers.h" +#include "CertStoreBearSSL.h" + +namespace BearSSL { + +class WiFiClientSecure_light : public WiFiClient { + public: + WiFiClientSecure_light(int recv, int xmit); + ~WiFiClientSecure_light() override; + + void allocateBuffers(void); + + int connect(IPAddress ip, uint16_t port) override; + int connect(const String& host, uint16_t port) override; + int connect(const char* name, uint16_t port) override; + + uint8_t connected() override; + size_t write(const uint8_t *buf, size_t size) override; + size_t write_P(PGM_P buf, size_t size) override; + size_t write(const char *buf) { + return write((const uint8_t*)buf, strlen(buf)); + } + size_t write_P(const char *buf) { + return write_P((PGM_P)buf, strlen_P(buf)); + } + size_t write(Stream& stream); // Note this is not virtual + int read(uint8_t *buf, size_t size) override; + int available() override; + int read() override; + int peek() override; + size_t peekBytes(uint8_t *buffer, size_t length) override; + bool flush(unsigned int maxWaitMs); + bool stop(unsigned int maxWaitMs); + void flush() override { (void)flush(0); } + void stop() override { (void)stop(0); } + + // Only check SHA1 fingerprint of public key + void setPubKeyFingerprint(const uint8_t *f1, const uint8_t *f2, + bool f_any = false) { + _fingerprint1 = f1; + _fingerprint2 = f2; + _fingerprint_any = f_any; + } + const uint8_t * getRecvPubKeyFingerprint(void) { + return _recv_fingerprint; + } + + void setClientECCert(const br_x509_certificate *cert, const br_ec_private_key *sk, + unsigned allowed_usages, unsigned cert_issuer_key_type); + + // Sets the requested buffer size for transmit and receive + void setBufferSizes(int recv, int xmit); + + // Returns whether MFLN negotiation for the above buffer sizes succeeded (after connection) + int getMFLNStatus() { + return connected() && br_ssl_engine_get_mfln_negotiated(_eng); + } + + int32_t getLastError(void) { + if (_last_error) { + return _last_error; + } else { + return br_ssl_engine_last_error(_eng); + } + } + inline void setLastError(int32_t err) { + _last_error = err; + } + inline void clearLastError(void) { + _last_error = 0; + } + + private: + void _clear(); + bool _ctx_present; + std::shared_ptr _sc; + inline bool ctx_present() { + return _ctx_present; + } + br_ssl_engine_context *_eng; // &_sc->eng, to allow for client or server contexts + std::shared_ptr _iobuf_in; + std::shared_ptr _iobuf_out; + time_t _now; + int _iobuf_in_size; + int _iobuf_out_size; + bool _handshake_done; + uint64_t _last_error; + + bool _fingerprint_any; // accept all fingerprints + const uint8_t *_fingerprint1; // fingerprint1 to be checked against + const uint8_t *_fingerprint2; // fingerprint2 to be checked against + uint8_t _recv_fingerprint[20]; // fingerprint received + + unsigned char *_recvapp_buf; + size_t _recvapp_len; + + bool _clientConnected(); // Is the underlying socket alive? + bool _connectSSL(const char *hostName); // Do initial SSL handshake + void _freeSSL(); + int _run_until(unsigned target, bool blocking = true); + size_t _write(const uint8_t *buf, size_t size, bool pmem); + bool _wait_for_handshake(); // Sets and return the _handshake_done after connecting + + // Optional client certificate + br_x509_certificate _chain; // local RAM copy + const br_x509_certificate *_chain_P; // PROGMEM certificate + br_ec_private_key _sk_ec; + const br_ec_private_key *_sk_ec_P; // PROGMEM private key + unsigned _allowed_usages; + unsigned _cert_issuer_key_type; + +}; + +#define ERR_OOM -1000 +#define ERR_CANT_RESOLVE_IP -1001 +#define ERR_TCP_CONNECT -1002 + +}; + +#endif diff --git a/sonoff/my_user_config.h b/sonoff/my_user_config.h index 2dce8ed84..a87a8f79d 100644 --- a/sonoff/my_user_config.h +++ b/sonoff/my_user_config.h @@ -88,7 +88,7 @@ #define MQTT_USE 1 // [SetOption3] Select default MQTT use (0 = Off, 1 = On) #define MQTT_HOST "" // [MqttHost] -#define MQTT_FINGERPRINT1 "A5 02 FF 13 99 9F 8B 39 8E F1 83 4F 11 23 65 0B 32 36 FC 07" // [MqttFingerprint1] +#define MQTT_FINGERPRINT1 "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" // [MqttFingerprint1] #define MQTT_FINGERPRINT2 "A5 02 FF 13 99 9F 8B 39 8E F1 83 4F 11 23 65 0B 32 36 FC 07" // [MqttFingerprint2] #define MQTT_PORT 1883 // [MqttPort] MQTT port (10123 on CloudMQTT) #define MQTT_USER "DVES_USER" // [MqttUser] MQTT user @@ -267,6 +267,11 @@ //#define USE_MQTT_TLS // Use TLS for MQTT connection (+53k code, +15k mem) // #define USE_MQTT_TLS_CA_CERT // Use LetsEncrypt Certificate from sonoff_letsencrypt.h - Not supported with core 2.3.0 +// -- MQTT - Special version for AWS IoT +//#define USE_MQTT_AWS_IOT // Enable MQTT for AWS IoT - requires a private key (+56.7k code, +6.0k mem and +6.6k additional during connection handshake) + // you need to generate a private key + certificate per device + // and update 'sonoff/sonoff_aws_iot.cpp' + // -- KNX IP Protocol ----------------------------- //#define USE_KNX // Enable KNX IP Protocol Support (+9.4k code, +3k7 mem) #define USE_KNX_WEB_MENU // Enable KNX WEB MENU (+8.3k code, +144 mem) @@ -473,4 +478,18 @@ #error "Select either USE_MQTT_TLS or USE_WEBSERVER as there is just not enough memory to play with" #endif +#if defined(USE_MQTT_TLS) && defined(USE_MQTT_AWS_IOT) + #error "Select either USE_MQTT_TLS or USE_MQTT_AWS_IOT, they are not compatible" +#endif + +#if defined(USE_DISCOVERY) && defined(USE_MQTT_AWS_IOT) + #error "Select either USE_DISCOVERY or USE_MQTT_AWS_IOT, mDNS takes too much code space and is not needed for AWS IoT" +#endif + + +#if defined(USE_MQTT_TLS) || defined(USE_MQTT_AWS_IOT) + #undef WEB_LOG_SIZE + #define WEB_LOG_SIZE (2000) // reduce log buffer size when using TLS +#endif + #endif // _MY_USER_CONFIG_H_ diff --git a/sonoff/sonoff.h b/sonoff/sonoff.h index d6c4f20b1..27b22202e 100644 --- a/sonoff/sonoff.h +++ b/sonoff/sonoff.h @@ -120,11 +120,7 @@ const uint16_t MIN_MESSZ = 893; // Min number of characters in MQTT const uint8_t SENSOR_MAX_MISS = 5; // Max number of missed sensor reads before deciding it's offline -#ifdef USE_MQTT_TLS - const uint16_t WEB_LOG_SIZE = 2000; // Max number of characters in weblog -#else - const uint16_t WEB_LOG_SIZE = 4000; // Max number of characters in weblog -#endif +#define WEB_LOG_SIZE (4000) // Max number of characters in weblog const uint8_t MAX_BACKLOG = 30; // Max number of commands in backlog const uint32_t MIN_BACKLOG_DELAY = 2; // Minimal backlog delay in 0.1 seconds diff --git a/sonoff/sonoff_aws_iot.cpp b/sonoff/sonoff_aws_iot.cpp new file mode 100644 index 000000000..1bb476e61 --- /dev/null +++ b/sonoff/sonoff_aws_iot.cpp @@ -0,0 +1,147 @@ +/* + sonoff_aws_iot.cpp - TLS AWS IoT for Sonoff-Tasmota - Private key + + Copyright (C) 2019 Theo Arends + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include + +// nasty hack to force PROGMEM +#define static static PROGMEM + +namespace aws_iot_privkey { +/*********************************************************************************************\ + * Private key for AWS IoT + * + * Create the private key, generate the CSR and sign it with AWS IoT Console. + * + * Then generate C version of Private Key and Certificate, cut and paste below + * + * Downloaded from https://www.identrust.com/support/downloads +\*********************************************************************************************/ + +/*********************************************************************************************\ + * Export Private Key as a C structure + * + * $ bearssl skey -C +\*********************************************************************************************/ + +/* --------------- CUT AND PASTE PRIVATE KEY BELOW --------------- */ +/* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ */ + +static const unsigned char EC_X[] = { + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77 +}; + +static const br_ec_private_key EC = { + 23, + (unsigned char *)EC_X, sizeof EC_X +}; + +/* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ */ +/* --------------- CUT AND PASTE PRIVATE KEY ABOVE --------------- */ + +/*********************************************************************************************\ + * Export Private Key as a C structure + * + * $ bearssl chain +\*********************************************************************************************/ + +/* --------------- CUT AND PASTE PRIVATE KEY BELOW --------------- */ +/* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ */ + +static const unsigned char CERT0[] = { + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77 +}; + +static const br_x509_certificate CHAIN[] = { + { (unsigned char *)CERT0, sizeof CERT0 } +}; + +#define CHAIN_LEN 1 + +/* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ */ +/* --------------- CUT AND PASTE PRIVATE KEY ABOVE --------------- */ + +const br_ec_private_key *AWS_IoT_Private_Key = &EC; +const br_x509_certificate *AWS_IoT_Client_Certificate = &CHAIN[0]; + +} diff --git a/sonoff/sonoff_post.h b/sonoff/sonoff_post.h index 6089adfa5..aa2b68656 100644 --- a/sonoff/sonoff_post.h +++ b/sonoff/sonoff_post.h @@ -419,7 +419,8 @@ void KNX_CB_Action(message_t const &msg, void *arg); #endif #ifndef MQTT_FINGERPRINT1 -#define MQTT_FINGERPRINT1 "A5 02 FF 13 99 9F 8B 39 8E F1 83 4F 11 23 65 0B 32 36 FC 07" +// Set an all-zeros default fingerprint to activate auto-learning on first connection (AWS IoT) +#define MQTT_FINGERPRINT1 "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" #endif #ifndef MQTT_FINGERPRINT2 @@ -434,7 +435,8 @@ void KNX_CB_Action(message_t const &msg, void *arg); #define MQTT_MAX_PACKET_SIZE 1000 // Bytes #endif #ifndef MQTT_KEEPALIVE -#define MQTT_KEEPALIVE 15 // Seconds +//#define MQTT_KEEPALIVE 15 // Seconds +#define MQTT_KEEPALIVE 30 // Changed to 30s which is min for AWS IoT, hoping it does not break anthing #endif #ifndef MQTT_TIMEOUT #define MQTT_TIMEOUT 10000 // milli seconds diff --git a/sonoff/xdrv_02_mqtt.ino b/sonoff/xdrv_02_mqtt.ino index 23f9ebe5d..10852cad8 100644 --- a/sonoff/xdrv_02_mqtt.ino +++ b/sonoff/xdrv_02_mqtt.ino @@ -24,6 +24,9 @@ #include "sonoff_letsencrypt.h" // LetsEncrypt certificate #endif WiFiClientSecure EspClient; // Wifi Secure Client +#elif defined(USE_MQTT_AWS_IOT) + #include "WiFiClientSecureLightBearSSL.h" + BearSSL::WiFiClientSecure_light *awsClient; #else WiFiClient EspClient; // Wifi Client #endif @@ -46,6 +49,32 @@ uint8_t mqtt_initial_connection_state = 2; // MQTT connection messages state bool mqtt_connected = false; // MQTT virtual connection status bool mqtt_allowed = false; // MQTT enabled and parameters valid +#ifdef USE_MQTT_AWS_IOT + +namespace aws_iot_privkey { + // this is where the Private Key and Certificate are stored + extern const br_ec_private_key *AWS_IoT_Private_Key; + extern const br_x509_certificate *AWS_IoT_Client_Certificate; +} + +// A typical AWS IoT endpoint is 50 characters long, it does not fit +// in MqttHost field (32 chars). We need to concatenate both MqttUser and MqttHost +char AWS_endpoint[65]; // aWS IOT endpoint, concatenation of user and host + +// check whether the fingerprint is filled with a single value +// Filled with 0x00 = accept any fingerprint and learn it for next time +// Filled with 0xFF = accept any fingerpring forever +bool is_fingerprint_mono_value(uint8_t finger[20], uint8_t value) { + for (uint32_t i = 0; i<20; i++) { + if (finger[i] != value) { + return false; + } + } + return true; +} + +#endif // USE_MQTT_AWS_IOT + /*********************************************************************************************\ * MQTT driver specific code need to provide the following functions: * @@ -62,7 +91,35 @@ bool mqtt_allowed = false; // MQTT enabled and parameters valid #error "MQTT_MAX_PACKET_SIZE is too small in libraries/PubSubClient/src/PubSubClient.h, increase it to at least 1000" #endif +#ifdef USE_MQTT_AWS_IOT +PubSubClient MqttClient; +#else PubSubClient MqttClient(EspClient); +#endif + + +void MqttInit(void) { +#ifdef USE_MQTT_AWS_IOT + AWS_endpoint[0] = 0; + uint8_t len_user = strlen(Settings.mqtt_user); + uint8_t len_host = strlen(Settings.mqtt_host); + if (len_user > 0) { + strcpy(AWS_endpoint, Settings.mqtt_user); + if (('.' != AWS_endpoint[len_user-1]) && ('.' != Settings.mqtt_host[0])) { + AWS_endpoint[len_user++] = '.'; + } + strcpy(&AWS_endpoint[len_user], Settings.mqtt_host); + } + + awsClient = new BearSSL::WiFiClientSecure_light(1024,1024); + awsClient->setClientECCert(aws_iot_privkey::AWS_IoT_Client_Certificate, + aws_iot_privkey::AWS_IoT_Private_Key, + 0xFFFF /* all usages, don't care */, 0); + + MqttClient.setClient(*awsClient); +#endif +} + bool MqttIsConnected(void) { @@ -179,6 +236,10 @@ void MqttPublishDirect(const char* topic, bool retained) void MqttPublish(const char* topic, bool retained) { char *me; +#ifdef USE_MQTT_AWS_IOT + AddLog_P(LOG_LEVEL_INFO, S_LOG_MQTT, PSTR("Retained is not supported by AWS IoT, using retained = false.")); + retained = false; // AWS IoT does not support retained, it will disconnect if received +#endif if (!strcmp(Settings.mqtt_prefix[0],Settings.mqtt_prefix[1])) { me = strstr(topic,Settings.mqtt_prefix[0]); @@ -282,7 +343,11 @@ void MqttDisconnected(int state) mqtt_connected = false; mqtt_retry_counter = Settings.mqtt_retry; +#ifdef USE_MQTT_AWS_IOT + AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_MQTT D_CONNECT_FAILED_TO " %s:%d, rc %d. " D_RETRY_IN " %d " D_UNIT_SECOND), AWS_endpoint, Settings.mqtt_port, state, mqtt_retry_counter); +#else AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_MQTT D_CONNECT_FAILED_TO " %s:%d, rc %d. " D_RETRY_IN " %d " D_UNIT_SECOND), Settings.mqtt_host, Settings.mqtt_port, state, mqtt_retry_counter); +#endif rules_flag.mqtt_disconnected = 1; } @@ -443,6 +508,8 @@ void MqttReconnect(void) #ifdef USE_MQTT_TLS EspClient = WiFiClientSecure(); // Wifi Secure Client reconnect issue 4497 (https://github.com/esp8266/Arduino/issues/4497) +#elif defined(USE_MQTT_AWS_IOT) + awsClient->stop(); #else EspClient = WiFiClient(); // Wifi Client reconnect issue 4497 (https://github.com/esp8266/Arduino/issues/4497) #endif @@ -456,7 +523,11 @@ void MqttReconnect(void) } MqttClient.setCallback(MqttDataHandler); +#ifdef USE_MQTT_AWS_IOT + MqttClient.setServer(AWS_endpoint, Settings.mqtt_port); +#else MqttClient.setServer(Settings.mqtt_host, Settings.mqtt_port); +#endif /* // Skip MQTT host DNS lookup if not needed uint32_t current_hash = GetHash(Settings.mqtt_host, strlen(Settings.mqtt_host)); @@ -466,9 +537,50 @@ void MqttReconnect(void) } MqttClient.setServer(mqtt_host_addr, Settings.mqtt_port); */ + uint32_t time = millis(); +#ifdef USE_MQTT_AWS_IOT + bool allow_all_fingerprints = false; + bool learn_fingerprint1 = is_fingerprint_mono_value(Settings.mqtt_fingerprint[0], 0x00); + bool learn_fingerprint2 = is_fingerprint_mono_value(Settings.mqtt_fingerprint[1], 0x00); + allow_all_fingerprints |= is_fingerprint_mono_value(Settings.mqtt_fingerprint[0], 0xff); + allow_all_fingerprints |= is_fingerprint_mono_value(Settings.mqtt_fingerprint[1], 0xff); + allow_all_fingerprints |= learn_fingerprint1; + allow_all_fingerprints |= learn_fingerprint2; + awsClient->setPubKeyFingerprint(Settings.mqtt_fingerprint[0], Settings.mqtt_fingerprint[1], allow_all_fingerprints); + AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_MQTT "AWS IoT endpoint: %s"), AWS_endpoint); + if (MqttClient.connect(mqtt_client, mqtt_user, mqtt_pwd, nullptr, 0, false, nullptr)) { +#else if (MqttClient.connect(mqtt_client, mqtt_user, mqtt_pwd, stopic, 1, true, mqtt_data)) { +#endif +#ifdef USE_MQTT_AWS_IOT + AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_MQTT "AWS IoT connected in %d ms"), millis() - time); + if (learn_fingerprint1 || learn_fingerprint2) { + // we potentially need to learn the fingerprint just seen + bool fingerprint_matched = false; + const uint8_t *recv_fingerprint = awsClient->getRecvPubKeyFingerprint(); + if (0 == memcmp(recv_fingerprint, Settings.mqtt_fingerprint[0], 20)) { + fingerprint_matched = true; + } + if (0 == memcmp(recv_fingerprint, Settings.mqtt_fingerprint[1], 20)) { + fingerprint_matched = true; + } + if (!fingerprint_matched) { + // we had no match, so we need to change all fingerprints ready to learn + if (learn_fingerprint1) { + memcpy(Settings.mqtt_fingerprint[0], recv_fingerprint, 20); + } + if (learn_fingerprint2) { + memcpy(Settings.mqtt_fingerprint[1], recv_fingerprint, 20); + } + restart_flag = 2; // save and restart + } + } +#endif MqttConnected(); } else { +#ifdef USE_MQTT_AWS_IOT + AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_MQTT "AWS IoT connection error: %d"), awsClient->getLastError()); +#endif MqttDisconnected(MqttClient.state()); // status codes are documented here http://pubsubclient.knolleary.net/api.html#state } } @@ -549,7 +661,7 @@ bool MqttCommand(void) } Response_P(S_JSON_COMMAND_INDEX_SVALUE, command, index, GetStateText(index -1)); } -#ifdef USE_MQTT_TLS +#if defined(USE_MQTT_TLS) || defined(USE_MQTT_AWS_IOT) else if ((CMND_MQTTFINGERPRINT == command_code) && (index > 0) && (index <= 2)) { char fingerprint[60]; if ((data_len > 0) && (data_len < sizeof(fingerprint))) { @@ -827,6 +939,11 @@ bool Xdrv02(uint8_t function) if (Settings.flag.mqtt_enabled) { switch (function) { +#ifdef USE_MQTT_AWS_IOT + case FUNC_PRE_INIT: + MqttInit(); + break; +#endif case FUNC_EVERY_50_MSECOND: // https://github.com/knolleary/pubsubclient/issues/556 MqttClient.loop(); break;