diff --git a/bin/device.min.htm b/bin/device.min.htm index af7f532..b98864f 100644 --- a/bin/device.min.htm +++ b/bin/device.min.htm @@ -1 +1 @@ -Beer Gravity Monitor

Current version:
Loading...
Host name:
Loading...
Device ID:
Loading...

(C) Copyright 2021-22 Magnus Persson
\ No newline at end of file +Beer Gravity Monitor

Current version:
Loading...
Host name:
Loading...
Device ID:
Loading...
Certificates:
Loading...

(C) Copyright 2021-22 Magnus Persson
\ No newline at end of file diff --git a/data/certs.ar b/data/certs.ar new file mode 100644 index 0000000..d2cee3e Binary files /dev/null and b/data/certs.ar differ diff --git a/html/device.htm b/html/device.htm index 7dad2be..0147a6c 100644 --- a/html/device.htm +++ b/html/device.htm @@ -88,6 +88,10 @@
Device ID:
Loading...
+
+
Certificates:
+
Loading...
+

@@ -103,6 +107,11 @@ $("#app-ver").text(cfg["app-ver"] + " (html 0.6.0)"); $("#mdns").text(cfg["mdns"]); $("#id").text(cfg["id"]); + + if( cfg["certs"] ) + $("#certs").text("CA Store installed."); + else + $("#certs").text("CA Store NOT installed."); }) .fail(function () { showError('Unable to get data from the device.'); diff --git a/html/device.min.htm b/html/device.min.htm index af7f532..b98864f 100644 --- a/html/device.min.htm +++ b/html/device.min.htm @@ -1 +1 @@ -Beer Gravity Monitor

Current version:
Loading...
Host name:
Loading...
Device ID:
Loading...

(C) Copyright 2021-22 Magnus Persson
\ No newline at end of file +Beer Gravity Monitor

Current version:
Loading...
Host name:
Loading...
Device ID:
Loading...
Certificates:
Loading...

(C) Copyright 2021-22 Magnus Persson
\ No newline at end of file diff --git a/html/upload.htm b/html/upload.htm index ed73a73..65246ed 100644 --- a/html/upload.htm +++ b/html/upload.htm @@ -82,6 +82,10 @@
about.min.htm
Checking...
+
+
certs.ar
+
Checking...
+
@@ -137,6 +141,10 @@ function getUpload() { else $("#about").text("File is missing."); + if( cfg["certs"] ) + $("#certs").text("Completed."); + else + $("#certs").text("File is missing (optional)."); }) .fail(function () { diff --git a/html/upload.min.htm b/html/upload.min.htm index 1ee94dd..91db69c 100644 --- a/html/upload.min.htm +++ b/html/upload.min.htm @@ -1 +1 @@ -Beer Gravity Monitor

The listed files below needs to be uploaded to the FileSystem in order for the GUI to work. You can also flash the LittleFS filesystem but in that case you will loose your device settings. An OTA upgrade will automatically download the files if they are found in the same location as the firmware.bin. This page is a fallback option.

Once all the files are confirmed, please reboot the device for normal operation.
index.min.htm
Checking...
device.min.htm
Checking...
config.min.htm
Checking...
calibration.min.htm
Checking...
about.min.htm
Checking...

(C) Copyright 2021-22 Magnus Persson
\ No newline at end of file +Beer Gravity Monitor

The listed files below needs to be uploaded to the FileSystem in order for the GUI to work. You can also flash the LittleFS filesystem but in that case you will loose your device settings. An OTA upgrade will automatically download the files if they are found in the same location as the firmware.bin. This page is a fallback option.

Once all the files are confirmed, please reboot the device for normal operation.
index.min.htm
Checking...
device.min.htm
Checking...
config.min.htm
Checking...
calibration.min.htm
Checking...
about.min.htm
Checking...
certs.ar
Checking...

(C) Copyright 2021-22 Magnus Persson
\ No newline at end of file diff --git a/script/create_cert.py b/script/create_cert.py new file mode 100644 index 0000000..3cd52f2 --- /dev/null +++ b/script/create_cert.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 + +# This script pulls the list of Mozilla trusted certificate authorities +# from the web at the "mozurl" below, parses the file to grab the PEM +# for each cert, and then generates DER files in a new ./data directory +# Upload these to an on-chip filesystem and use the CertManager to parse +# and use them for your outgoing SSL connections. +# +# Script by Earle F. Philhower, III. Released to the public domain. +from __future__ import print_function +import csv +import os +import sys +from shutil import which + +# Change the path to the installed files. +arCmd = "C:\\Users\\magnu\\.platformio\\packages\\toolchain-xtensa\\bin\\xtensa-lx106-elf-ar.exe" +opensslCmd = "C:\\Program Files\\Git\\usr\\bin\\openssl.exe" + +from subprocess import Popen, PIPE, call +try: + from urllib.request import urlopen +except Exception: + from urllib2 import urlopen +try: + from StringIO import StringIO +except Exception: + from io import StringIO + +# check if ar and openssl are available +#if which('ar') is None and not os.path.isfile('./ar') and not os.path.isfile('./ar.exe'): +# raise Exception("You need the program 'ar' from xtensa-lx106-elf found here: (esp8266-arduino-core)/hardware/esp8266com/esp8266/tools/xtensa-lx106-elf/xtensa-lx106-elf/bin/ar") +#if which('openssl') is None and not os.path.isfile('./openssl') and not os.path.isfile('./openssl.exe'): +# raise Exception("You need to have openssl in PATH, installable from https://www.openssl.org/") + +# Mozilla's URL for the CSV file with included PEM certs +mozurl = "https://ccadb-public.secure.force.com/mozilla/IncludedCACertificateReportPEMCSV" + +# Load the names[] and pems[] array from the URL +names = [] +pems = [] +response = urlopen(mozurl) +csvData = response.read() +if sys.version_info[0] > 2: + csvData = csvData.decode('utf-8') +csvFile = StringIO(csvData) +csvReader = csv.reader(csvFile) +for row in csvReader: + names.append(row[0]+":"+row[1]+":"+row[2]) + for item in row: + if item.startswith("'-----BEGIN CERTIFICATE-----"): + pems.append(item) +del names[0] # Remove headers +del pems[0] # Remove headers + +# Try and make ./data, skip if present +try: + os.mkdir("../data") +except Exception: + pass + +derFiles = [] +idx = 0 +# Process the text PEM using openssl into DER files +for i in range(0, len(pems)): + certName = "../data/ca_%03d.der" % (idx); + thisPem = pems[i].replace("'", "") + print(names[i] + " -> " + certName) + ssl = Popen([opensslCmd,'x509','-inform','PEM','-outform','DER','-out', certName], shell = False, stdin = PIPE) + pipe = ssl.stdin + pipe.write(thisPem.encode('utf-8')) + pipe.close() + ssl.wait() + if os.path.exists(certName): + derFiles.append(certName) + idx = idx + 1 + +if os.path.exists("../data/certs.ar"): + os.unlink("../data/certs.ar"); + +arCmd = [arCmd, 'q', '../data/certs.ar'] + derFiles; +call( arCmd ) + +for der in derFiles: + os.unlink(der) \ No newline at end of file diff --git a/src/config.hpp b/src/config.hpp index 9b85505..6b07b74 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -89,6 +89,7 @@ SOFTWARE. #define CFG_PARAM_BATTERY "battery" #define CFG_PARAM_SLEEP_MODE "sleep-mode" #define CFG_PARAM_RSSI "rssi" +#define CFG_PARAM_CERTS "certs" // Used for holding sensordata or sensoroffsets struct RawGyroData { @@ -179,6 +180,7 @@ class Config { saveNeeded = true; } bool isOtaActive() { return otaURL.length() ? true : false; } + bool isOtaSecure() { return otaURL.startsWith("https://"); } const char* getWifiSSID() { return wifiSSID.c_str(); } void setWifiSSID(String s) { @@ -208,12 +210,14 @@ class Config { saveNeeded = true; } bool isHttpActive() { return httpPushUrl.length() ? true : false; } + bool isHttpSecure() { return httpPushUrl.startsWith("https://"); } const char* getHttpPushUrl2() { return httpPushUrl2.c_str(); } void setHttpPushUrl2(String s) { httpPushUrl2 = s; saveNeeded = true; } bool isHttpActive2() { return httpPushUrl2.length() ? true : false; } + bool isHttpSecure2() { return httpPushUrl2.startsWith("https://"); } // InfluxDB2 const char* getInfluxDb2PushUrl() { return influxDb2Url.c_str(); } @@ -240,6 +244,7 @@ class Config { // MQTT bool isMqttActive() { return mqttUrl.length() ? true : false; } + bool isMqttSecure() { return mqttUrl.endsWith(":8883"); } const char* getMqttUrl() { return mqttUrl.c_str(); } void setMqttUrl(String s) { mqttUrl = s; diff --git a/src/main.cpp b/src/main.cpp index 1872ae9..621333d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -180,6 +180,16 @@ void setup() { break; } + // Check if we need SSL for any of the push targets + if (myConfig.isHttpSecure() || myConfig.isHttpSecure2() || myConfig.isMqttSecure()) { + LOG_PERF_START("main-cert-store"); + myWifi.initCertstore(); + LOG_PERF_STOP("main-cert-store"); + LOG_PERF_START("main-cert-ntp"); + myWifi.initNTP(); + LOG_PERF_STOP("main-cert-ntp"); + } + LOG_PERF_STOP("main-setup"); Log.notice(F("Main: Setup completed." CR)); stableGyroMillis = millis(); // Dont include time for wifi connection diff --git a/src/pushtarget.cpp b/src/pushtarget.cpp index f57abc5..108a4c0 100644 --- a/src/pushtarget.cpp +++ b/src/pushtarget.cpp @@ -21,11 +21,11 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +#include #include - #include #include -#include +#include PushTarget myPushTarget; @@ -243,10 +243,24 @@ void PushTarget::sendHttp(String serverPath, float angle, float gravity, createIspindleFormat(doc, angle, gravity, corrGravity, temp, runTime); WiFiClient client; + WiFiClientSecure clientSecure; HTTPClient http; - // Your Domain name with URL path or IP address with path - http.begin(client, serverPath); + if (serverPath.startsWith("https://")) { + /*if (myWifi.getCertCount() > 0) { + // Allow secure channel, with CA validation + clientSecure.setCertStore(myWifi.getCertStore()); + Log.notice(F("PUSH: SSL enabled using certificate store." CR)); + } else*/ { + // Allow secure channel, but without certificate validation + clientSecure.setInsecure(); + Log.notice(F("PUSH: SSL enabled without validation." CR)); + } + http.begin(clientSecure, serverPath); + } else { + http.begin(client, serverPath); + } + String json; serializeJson(doc, json); #if LOG_LEVEL == 6 && !defined(PUSH_DISABLE_LOGGING) @@ -283,9 +297,26 @@ void PushTarget::sendMqtt(float angle, float gravity, float corrGravity, createIspindleFormat(doc, angle, gravity, corrGravity, temp, runTime); WiFiClient client; - MQTTClient mqtt(512); // Maximum message size + WiFiClientSecure clientSecure; + MQTTClient mqtt(512); // Maximum message size + + if (myConfig.isMqttSecure()) { + if (myWifi.getCertCount() > 0) { + // Allow secure channel, with CA validation + clientSecure.setCertStore(myWifi.getCertStore()); + Log.notice(F("PUSH: SSL enabled using certificate store." CR)); + } else { + // Allow secure channel, but without certificate validation + clientSecure.setInsecure(); + Log.notice(F("PUSH: SSL enabled without validation." CR)); + } + String url = myConfig.getMqttUrl(); + url.replace(":8883", ""); + mqtt.begin(url.c_str(), 8883, clientSecure); + } else { + mqtt.begin(myConfig.getMqttUrl(), client); + } - mqtt.begin(myConfig.getMqttUrl(), client); mqtt.connect(myConfig.getMDNS(), myConfig.getMqttUser(), myConfig.getMqttPass()); diff --git a/src/resources.cpp b/src/resources.cpp index f4b93e7..841cd5e 100644 --- a/src/resources.cpp +++ b/src/resources.cpp @@ -32,12 +32,9 @@ INCBIN(DeviceHtm, "data/device.min.htm"); INCBIN(ConfigHtm, "data/config.min.htm"); INCBIN(CalibrationHtm, "data/calibration.min.htm"); INCBIN(AboutHtm, "data/about.min.htm"); - -#else - -// Minium web interface for uploading htm files -INCBIN(UploadHtm, "data/upload.min.htm"); - #endif +// Minium web interface for uploading htm files, also used to upload certificate store. +INCBIN(UploadHtm, "data/upload.min.htm"); + // EOF diff --git a/src/webserver.cpp b/src/webserver.cpp index dd60bb6..4ec5d50 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -49,6 +49,7 @@ void WebServer::webHandleDevice() { doc[CFG_PARAM_APP_NAME] = CFG_APPNAME; doc[CFG_PARAM_APP_VER] = CFG_APPVER; doc[CFG_PARAM_MDNS] = myConfig.getMDNS(); + doc[CFG_PARAM_CERTS] = checkHtmlFile(CA_CERTS); #if LOG_LEVEL == 6 serializeJson(doc, Serial); Serial.print(CR); @@ -108,6 +109,7 @@ void WebServer::webHandleUpload() { doc["config"] = myWebServer.checkHtmlFile(WebServer::HTML_CONFIG); doc["calibration"] = myWebServer.checkHtmlFile(WebServer::HTML_CALIBRATION); doc["about"] = myWebServer.checkHtmlFile(WebServer::HTML_ABOUT); + doc["certs"] = myWebServer.checkHtmlFile(WebServer::CA_CERTS); #if LOG_LEVEL == 6 && !defined(WEB_DISABLE_LOGGING) serializeJson(doc, Serial); @@ -134,7 +136,8 @@ void WebServer::webHandleUploadFile() { f.equalsIgnoreCase("device.min.htm") || f.equalsIgnoreCase("calibration.min.htm") || f.equalsIgnoreCase("config.min.htm") || - f.equalsIgnoreCase("about.min.htm")) { + f.equalsIgnoreCase("about.min.htm") || + f.equalsIgnoreCase("certs.ar")) { validFilename = true; } @@ -587,6 +590,8 @@ const char* WebServer::getHtmlFileName(HtmlFile item) { return "calibration.min.htm"; case HTML_ABOUT: return "about.min.htm"; + case CA_CERTS: + return "certs.ar"; } return ""; @@ -636,6 +641,7 @@ bool WebServer::setupWebServer() { server->on("/calibration.htm", std::bind(&WebServer::webReturnCalibrationHtm, this)); server->on("/about.htm", std::bind(&WebServer::webReturnAboutHtm, this)); + server->on("/upload.htm", std::bind(&WebServer::webReturnUploadHtm, this)); #else // Show files in the filessytem at startup diff --git a/src/webserver.hpp b/src/webserver.hpp index 9051d23..8646e93 100644 --- a/src/webserver.hpp +++ b/src/webserver.hpp @@ -37,9 +37,8 @@ INCBIN_EXTERN(DeviceHtm); INCBIN_EXTERN(ConfigHtm); INCBIN_EXTERN(CalibrationHtm); INCBIN_EXTERN(AboutHtm); -#else -INCBIN_EXTERN(UploadHtm); #endif +INCBIN_EXTERN(UploadHtm); // classes class WebServer { @@ -86,12 +85,11 @@ class WebServer { void webReturnAboutHtm() { server->send_P(200, "text/html", (const char*)gAboutHtmData, gAboutHtmSize); } -#else +#endif void webReturnUploadHtm() { server->send_P(200, "text/html", (const char*)gUploadHtmData, gUploadHtmSize); } -#endif public: enum HtmlFile { @@ -99,7 +97,8 @@ class WebServer { HTML_DEVICE = 1, HTML_CONFIG = 2, HTML_ABOUT = 3, - HTML_CALIBRATION = 4 + HTML_CALIBRATION = 4, + CA_CERTS = 5 }; bool setupWebServer(); diff --git a/src/wifi.cpp b/src/wifi.cpp index e040735..068bc47 100644 --- a/src/wifi.cpp +++ b/src/wifi.cpp @@ -33,6 +33,7 @@ SOFTWARE. #include #include #include +#warning "Implement SSL for OTA" // Settings for DRD #define ESP_DRD_USE_LITTLEFS true @@ -70,6 +71,31 @@ const char *userPWD = USER_SSID_PWD; const int PIN_LED = 2; +// +// Initialize the certificate store +// +void WifiConnection::initCertstore() { + _certCount = _certStore.initCertStore(LittleFS, "/certs.idx", "/certs.ar"); + Log.notice(F("WIFI: Number of CA certs read: %d." CR), _certCount); +} + +// Set time via NTP, as required for x.509 validation +void WifiConnection::initNTP() { + configTime(3 * 3600, 0, "pool.ntp.org", "time.nist.gov"); + + Log.notice(F("WIFI: Waiting for NTP time sync." CR)); + time_t now = time(nullptr); + while (now < 8 * 3600 * 2) { + delay(500); + Serial.print("."); + now = time(nullptr); + } + Serial.println(); + struct tm timeinfo; + gmtime_r(&now, &timeinfo); + Log.notice(F("WIFI: Current time %s." CR), asctime(&timeinfo)); +} + // // Constructor // diff --git a/src/wifi.hpp b/src/wifi.hpp index 4854ffb..a92d06f 100644 --- a/src/wifi.hpp +++ b/src/wifi.hpp @@ -26,18 +26,23 @@ SOFTWARE. // Include #include +#include // classes class WifiConnection { private: + // SSL + BearSSL::CertStore _certStore; + int _certCount = 0; + // WIFI + void connectAsync(); + bool waitForConnection(int maxTime = 20); // OTA bool newFirmware = false; bool parseFirmwareVersionString(int (&num)[3], const char *version); void downloadFile(const char *fname); - void connectAsync(); - bool waitForConnection(int maxTime = 20); public: // WIFI @@ -53,6 +58,12 @@ class WifiConnection { void startPortal(); void loop(); + // SSL + void initCertstore(); + BearSSL::CertStore* getCertStore() { return &_certStore; } + int getCertCount() { return _certCount; } + void initNTP(); + // OTA bool updateFirmware(); bool checkFirmwareVersion(); diff --git a/test/device.json b/test/device.json index ea87d67..45cedae 100644 --- a/test/device.json +++ b/test/device.json @@ -2,5 +2,6 @@ "app-name": "GravityMon ", "app-ver": "0.0.0", "id": "7376ef", + "certs": true, "mdns": "gravmon" } \ No newline at end of file diff --git a/test/upload.json b/test/upload.json index 10d8f2b..be03ce0 100644 --- a/test/upload.json +++ b/test/upload.json @@ -3,5 +3,6 @@ "device": false, "config": false, "calibration": false, + "certs": false, "about": true } \ No newline at end of file