Skip to content

Commit 21a8dc7

Browse files
authored
Merge pull request #1365 from shadowguardian507-irl/AWS_IoT_Core_support
Server V3 (MQTT): AWS IoT Core support
2 parents 07b20ab + 5cffb59 commit 21a8dc7

6 files changed

Lines changed: 150 additions & 5 deletions

File tree

docs/source/userguide/components.rst

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,32 @@ Send all ``v.b`` metrics except ``v.b.soc``::
123123
OVMS# config set server.v3 metrics.include v.b.*
124124
OVMS# config set server.v3 metrics.exclude v.b.soc
125125

126+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
127+
Broker compatibility options
128+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
129+
130+
Some brokers have additional requirements:
131+
132+
- ``retain.depth.limit`` (default: ``no``) — omits the RETAIN flag on topics deeper than 8 path segments. Needed for AWS IoT Core, which silently drops retained publishes on topics deeper than 8 levels.
133+
134+
Example::
135+
136+
OVMS# config set server.v3 retain.depth.limit yes
137+
138+
.. note:: AWS IoT Core requires the MQTT keepalive to be 1200 seconds or less.
139+
Set ``updatetime.keepalive`` accordingly::
140+
141+
OVMS# config set server.v3 updatetime.keepalive 1200
142+
143+
These options can also be set from the **Config → Server V3 (MQTT)** web page.
144+
145+
^^^^^^^^^^^^^^^^^^^^^^^^^
146+
TLS client authentication
147+
^^^^^^^^^^^^^^^^^^^^^^^^^
148+
149+
The module supports client certificate authentication (mTLS) for brokers that need it.
150+
Paste the PEM certificate and key into the **Config → Server V3 (MQTT)** web page.
151+
126152
-------------------------------
127153
Upgrading from OVMS v1/v2 to v3
128154
-------------------------------

docs/source/userguide/ssltls.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ These trusted certificate authorities are used by the various module in the OVMS
4848
establishing SSL/TLS connections (in order to verify the certificate of the server being
4949
connected to).
5050

51+
.. note:: For MQTT brokers that require **client certificate authentication** (mTLS),
52+
see the :doc:`Server V3 configuration <components>` section on TLS client authentication.
53+
5154

5255
----------------------------------
5356
How to get the CA PEM for a Server

vehicle/OVMS.V3/changes.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
Open Vehicle Monitor System v3 - Change log
22

33
????-??-?? ??? ??????? OTA release
4+
- Server V3 (MQTT): AWS IoT Core compatibility fixes:
5+
- mTLS client certificate authentication for MQTT brokers that require it
6+
(e.g. AWS IoT Core). Configured via web UI (Config → Server V3).
7+
- Fixed MQTT CONNECT to omit empty username/password for certificate-only auth.
8+
- New option "retain.depth.limit": disables RETAIN on deep topics (>8 segments)
9+
for brokers like AWS IoT Core that reject them.
410
- Autostart: added init option "minimal" to boot with just basic networking as configured; this is
511
now the first fallback in case of repeated early crashes, aiming at keeping the module reachable
612
in case of a bug in a higher level module / configuration. Auto init will be disabled only if

vehicle/OVMS.V3/components/ovms_server_v3/src/ovms_server_v3.cpp

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ static const char *TAG = "ovms-server-v3";
3636
#include <vector>
3737
#include <algorithm>
3838
#include "ovms_server_v3.h"
39+
#include "ovms_utils.h"
3940
#include "buffered_shell.h"
4041
#include "ovms_command.h"
4142
#include "ovms_metrics.h"
@@ -96,8 +97,9 @@ static void OvmsServerV3MongooseCallback(struct mg_connection *nc, int ev, void
9697
ESP_LOGI(TAG, "Connection successful");
9798
struct mg_send_mqtt_handshake_opts opts;
9899
memset(&opts, 0, sizeof(opts));
99-
opts.user_name = MyOvmsServerV3->m_user.c_str();
100-
opts.password = MyOvmsServerV3->m_password.c_str();
100+
//If no user/password is set avoid sending an empty string
101+
opts.user_name = MyOvmsServerV3->m_user.empty() ? NULL : MyOvmsServerV3->m_user.c_str();
102+
opts.password = MyOvmsServerV3->m_password.empty() ? NULL : MyOvmsServerV3->m_password.c_str();
101103
opts.will_topic = MyOvmsServerV3->m_will_topic.c_str();
102104
opts.will_message = "no";
103105
opts.flags |= MG_MQTT_WILL_RETAIN;
@@ -237,6 +239,7 @@ OvmsServerV3::OvmsServerV3(const char* name)
237239
m_updatetime_sendall = 1200;
238240
m_updatetime_keepalive = 29*60;
239241
m_legacy_event_topic = true;
242+
m_retain_depth_limit = false;
240243
m_notify_info_pending = false;
241244
m_notify_error_pending = false;
242245
m_notify_alert_pending = false;
@@ -458,8 +461,22 @@ void OvmsServerV3::TransmitMetric(OvmsMetric* metric)
458461

459462
std::string val = metric->AsString();
460463

464+
// When retain.depth.limit is enabled, topics with more than 7 slashes (>8 segments)
465+
// are published without the RETAIN flag. This is required for AWS IoT Core, which
466+
// rejects retained publishes on topics deeper than 8 segments.
467+
int qos_flags;
468+
if (m_retain_depth_limit)
469+
{
470+
int slash_count = (int)std::count(topic.begin(), topic.end(), '/');
471+
qos_flags = MG_MQTT_QOS(0) | (slash_count <= 7 ? MG_MQTT_RETAIN : 0);
472+
}
473+
else
474+
{
475+
qos_flags = MG_MQTT_QOS(0) | MG_MQTT_RETAIN;
476+
}
477+
461478
mg_mqtt_publish(m_mgconn, topic.c_str(), NextMsgId(),
462-
MG_MQTT_QOS(0) | MG_MQTT_RETAIN, val.c_str(), val.length());
479+
qos_flags, val.c_str(), val.length());
463480
ESP_LOGV(TAG,"Tx metric %s=%s",topic.c_str(),val.c_str());
464481
}
465482

@@ -986,6 +1003,12 @@ void OvmsServerV3::Connect()
9861003
#if CONFIG_MG_ENABLE_SSL
9871004
opts.ssl_ca_cert = MyOvmsTLS.GetTrustedList();
9881005
opts.ssl_server_name = m_server.c_str();
1006+
if (path_exists("/store/tls/serverv3_client.crt") && path_exists("/store/tls/serverv3_client.key"))
1007+
{
1008+
opts.ssl_cert = "/store/tls/serverv3_client.crt";
1009+
opts.ssl_key = "/store/tls/serverv3_client.key";
1010+
ESP_LOGI(TAG, "Using MQTT mTLS client certificate authentication");
1011+
}
9891012
#else
9901013
ESP_LOGE(TAG, "mg_connect(%s) failed: SSL support disabled", address.c_str());
9911014
SetStatus("Error: Connection failed (SSL support disabled)", true, Undefined);
@@ -1187,6 +1210,7 @@ void OvmsServerV3::ConfigChanged(OvmsConfigParam* param)
11871210
m_updatetime_sendall = param->GetValueInt("updatetime.sendall", m_updatetime_sendall);
11881211
m_updatetime_keepalive = param->GetValueInt("updatetime.keepalive", m_updatetime_keepalive);
11891212
m_legacy_event_topic = param->GetValueBool("events.legacy_topic", true);
1213+
m_retain_depth_limit = param->GetValueBool("retain.depth.limit", m_retain_depth_limit);
11901214
m_updatetime_priority = param->GetValueBool("updatetime.priority", false);
11911215
m_updatetime_immediately = param->GetValueBool("updatetime.immediately", false);
11921216
m_max_per_call_sendall = param->GetValueInt("queue.sendall", m_max_per_call_sendall);

vehicle/OVMS.V3/components/ovms_server_v3/src/ovms_server_v3.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ class OvmsServerV3 : public OvmsServer, MongooseClient
114114
int m_max_per_call_modified;
115115
bool m_updatetime_priority;
116116
bool m_legacy_event_topic;
117+
bool m_retain_depth_limit;
117118
bool m_updatetime_immediately;
118119
std::atomic<bool> m_have_immediately;
119120
bool m_connection_available;

vehicle/OVMS.V3/components/ovms_webserver/src/web_cfg_server_v3.cpp

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,12 @@ void OvmsWebServer::HandleCfgServerV3(PageEntry_t& p, PageContext_t& c)
3636
auto lock = MyConfig.Lock();
3737
std::string error;
3838
std::string server, clientid, user, password, port, topic_prefix;
39+
extram::string client_cert, client_key;
3940
std::string updatetime_connected, updatetime_idle, updatetime_on;
4041
std::string updatetime_charging, updatetime_awake, updatetime_sendall, updatetime_keepalive;
4142
std::string metrics_priority, metrics_include, metrics_exclude, metrics_immediately, metrics_exclude_immediately;
4243
std::string queue_sendall, queue_modified;
43-
bool tls, legacy_event_topic, updatetime_priority, updatetime_immediately;
44+
bool tls, legacy_event_topic, updatetime_priority, updatetime_immediately, retain_depth_limit;
4445

4546
if (c.method == "POST") {
4647
// process form submission:
@@ -50,6 +51,8 @@ void OvmsWebServer::HandleCfgServerV3(PageEntry_t& p, PageContext_t& c)
5051
clientid = c.getvar("clientid");
5152
user = c.getvar("user");
5253
password = c.getvar("password");
54+
c.getvar("client_cert", client_cert);
55+
c.getvar("client_key", client_key);
5356
port = c.getvar("port");
5457
topic_prefix = c.getvar("topic_prefix");
5558
updatetime_connected = c.getvar("updatetime_connected");
@@ -61,6 +64,7 @@ void OvmsWebServer::HandleCfgServerV3(PageEntry_t& p, PageContext_t& c)
6164
updatetime_keepalive = c.getvar("updatetime_keepalive");
6265
updatetime_priority = (c.getvar("updatetime_priority") == "yes");
6366
updatetime_immediately = (c.getvar("updatetime_immediately") == "yes");
67+
retain_depth_limit = (c.getvar("retain_depth_limit") == "yes");
6468
metrics_priority = c.getvar("metrics_priority");
6569
metrics_include = c.getvar("metrics_include");
6670
metrics_exclude = c.getvar("metrics_exclude");
@@ -111,6 +115,18 @@ void OvmsWebServer::HandleCfgServerV3(PageEntry_t& p, PageContext_t& c)
111115
error += "<li data-input=\"updatetime_keepalive\">Keepalive interval must be at least 60 seconds</li>";
112116
}
113117
}
118+
if (!client_cert.empty() && !startsWith(client_cert, "-----BEGIN CERTIFICATE-----")) {
119+
error += "<li data-input=\"client_cert\">Client certificate must be in PEM CERTIFICATE format</li>";
120+
}
121+
if (!client_key.empty() &&
122+
!startsWith(client_key, "-----BEGIN PRIVATE KEY-----") &&
123+
!startsWith(client_key, "-----BEGIN RSA PRIVATE KEY-----") &&
124+
!startsWith(client_key, "-----BEGIN EC PRIVATE KEY-----")) {
125+
error += "<li data-input=\"client_key\">Client private key must be in PEM PRIVATE KEY format</li>";
126+
}
127+
if (client_cert.empty() != client_key.empty()) {
128+
error += "<li data-input=\"client_cert,client_key\">Both client certificate and private key must be given</li>";
129+
}
114130

115131
if (error == "") {
116132
// success:
@@ -132,14 +148,40 @@ void OvmsWebServer::HandleCfgServerV3(PageEntry_t& p, PageContext_t& c)
132148
MyConfig.SetParamValue("server.v3", "updatetime.keepalive", updatetime_keepalive);
133149
MyConfig.SetParamValueBool("server.v3", "updatetime.priority", updatetime_priority);
134150
MyConfig.SetParamValueBool("server.v3", "updatetime.immediately", updatetime_immediately);
151+
MyConfig.SetParamValueBool("server.v3", "retain.depth.limit", retain_depth_limit);
135152
MyConfig.SetParamValue("server.v3", "metrics.priority", metrics_priority);
136153
MyConfig.SetParamValue("server.v3", "metrics.include", metrics_include);
137154
MyConfig.SetParamValue("server.v3", "metrics.exclude", metrics_exclude);
138155
MyConfig.SetParamValue("server.v3", "metrics.include.immediately", metrics_immediately);
139156
MyConfig.SetParamValue("server.v3", "metrics.exclude.immediately", metrics_exclude_immediately);
140157
MyConfig.SetParamValue("server.v3", "queue.sendall", queue_sendall);
141158
MyConfig.SetParamValue("server.v3", "queue.modified", queue_modified);
142-
159+
if (client_cert.empty()) {
160+
unlink("/store/tls/serverv3_client.crt");
161+
unlink("/store/tls/serverv3_client.key");
162+
}
163+
else {
164+
if (save_file("/store/tls/serverv3_client.crt", client_cert) != 0) {
165+
error = "<li data-input=\"client_cert\">Error saving TLS certificate: ";
166+
error += strerror(errno);
167+
error += "</li>";
168+
error = "<p class=\"lead\">Error!</p><ul class=\"errorlist\">" + error + "</ul>";
169+
c.head(400);
170+
c.alert("danger", error.c_str());
171+
c.done();
172+
return;
173+
}
174+
if (save_file("/store/tls/serverv3_client.key", client_key) != 0) {
175+
error = "<li data-input=\"client_key\">Error saving TLS private key: ";
176+
error += strerror(errno);
177+
error += "</li>";
178+
error = "<p class=\"lead\">Error!</p><ul class=\"errorlist\">" + error + "</ul>";
179+
c.head(400);
180+
c.alert("danger", error.c_str());
181+
c.done();
182+
return;
183+
}
184+
}
143185
c.head(200);
144186
c.alert("success", "<p class=\"lead\">Server V3 (MQTT) connection configured.</p>");
145187
OutputHome(p, c);
@@ -160,6 +202,8 @@ void OvmsWebServer::HandleCfgServerV3(PageEntry_t& p, PageContext_t& c)
160202
clientid = MyConfig.GetParamValue("server.v3", "clientid");
161203
user = MyConfig.GetParamValue("server.v3", "user");
162204
password = MyConfig.GetParamValue("password", "server.v3");
205+
load_file("/store/tls/serverv3_client.crt", client_cert);
206+
load_file("/store/tls/serverv3_client.key", client_key);
163207
port = MyConfig.GetParamValue("server.v3", "port");
164208
topic_prefix = MyConfig.GetParamValue("server.v3", "topic.prefix");
165209
updatetime_connected = MyConfig.GetParamValue("server.v3", "updatetime.connected");
@@ -171,6 +215,7 @@ void OvmsWebServer::HandleCfgServerV3(PageEntry_t& p, PageContext_t& c)
171215
updatetime_keepalive = MyConfig.GetParamValue("server.v3", "updatetime.keepalive", "1740");
172216
updatetime_priority = MyConfig.GetParamValueBool("server.v3", "updatetime.priority", false);
173217
updatetime_immediately = MyConfig.GetParamValueBool("server.v3", "updatetime.immediately", false);
218+
retain_depth_limit = MyConfig.GetParamValueBool("server.v3", "retain.depth.limit", false);
174219
metrics_priority = MyConfig.GetParamValue("server.v3", "metrics.priority");
175220
metrics_include = MyConfig.GetParamValue("server.v3", "metrics.include");
176221
metrics_exclude = MyConfig.GetParamValue("server.v3", "metrics.exclude");
@@ -207,6 +252,46 @@ void OvmsWebServer::HandleCfgServerV3(PageEntry_t& p, PageContext_t& c)
207252
c.input_text("Topic Prefix", "topic_prefix", topic_prefix.c_str(),
208253
"optional, default: ovms/<username>/<vehicle id>/");
209254

255+
c.fieldset_start("TLS client authentication (optional)");
256+
c.printf(
257+
"<div class=\"form-group\">\n"
258+
"<label class=\"control-label col-sm-3\" for=\"input-content\">Client certificate:</label>\n"
259+
"<div class=\"col-sm-9\">\n"
260+
"<textarea class=\"form-control font-monospace\" style=\"font-size:80%%;white-space:pre;\"\n"
261+
"autocapitalize=\"none\" autocorrect=\"off\" autocomplete=\"off\" spellcheck=\"false\"\n"
262+
"placeholder=\"-----BEGIN CERTIFICATE-----&#13;&#10;...&#13;&#10;-----END CERTIFICATE-----\"\n"
263+
"rows=\"5\" id=\"input-client_cert\" name=\"client_cert\">%s</textarea>\n"
264+
"</div>\n"
265+
"</div>\n"
266+
, c.encode_html(client_cert).c_str());
267+
c.printf(
268+
"<div class=\"form-group\">\n"
269+
"<label class=\"control-label col-sm-3\" for=\"input-content\">Client private key:</label>\n"
270+
"<div class=\"col-sm-9\">\n"
271+
"<textarea class=\"form-control font-monospace\" style=\"font-size:80%%;white-space:pre;\"\n"
272+
"autocapitalize=\"none\" autocorrect=\"off\" autocomplete=\"off\" spellcheck=\"false\"\n"
273+
"placeholder=\"-----BEGIN PRIVATE KEY-----&#13;&#10;...&#13;&#10;-----END PRIVATE KEY-----\"\n"
274+
"rows=\"5\" id=\"input-client_key\" name=\"client_key\">%s</textarea>\n"
275+
"<p class=\"help-block\">Supports PKCS#8, RSA and EC PEM private keys. <br/>"
276+
" Leave both the certificate and key empty to authenticate with username/password only.</p>\n"
277+
"</div>\n"
278+
"</div>\n"
279+
, c.encode_html(client_key).c_str());
280+
c.fieldset_end();
281+
c.fieldset_start("AWS IoT Core (optional)");
282+
c.input_checkbox("Limit retain to 8-segment topics", "retain_depth_limit", retain_depth_limit,
283+
"<p>Omits the MQTT RETAIN flag on topics with more than 8 path segments."
284+
" Required for AWS IoT Core. Default: disabled.</p>");
285+
c.printf(
286+
"<div class=\"form-group\">\n"
287+
"<div class=\"col-sm-9 col-sm-offset-3\">\n"
288+
"<b> MQTT keepalive </b><br>\n"
289+
"<p class=\"help-block\">AWS IoT Core requires the MQTT keepalive to be 1200 seconds or less."
290+
" Set the <em>Keepalive</em> interval below to 1200 or lower.</p>\n"
291+
"</div>\n"
292+
"</div>\n");
293+
c.fieldset_end();
294+
210295
c.fieldset_start("Update intervals");
211296
c.input("number", "…idle", "updatetime_idle", updatetime_idle.c_str(), "default: 600", "default: 600, update interval when client not connected", "min=\"1\" max=\"1200\" step=\"1\"", "seconds");
212297
c.input("number", "…on", "updatetime_on", updatetime_on.c_str(), "default: 5", "default: 5, update interval when Car is on", "min=\"1\" max=\"600\" step=\"1\"", "seconds");

0 commit comments

Comments
 (0)