From ccce7eb8e2326f4daca788ac1a023b4c1c4b17f6 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 13:14:45 -0700 Subject: [PATCH 01/40] F-3825 F-4050 F-4051 F-4052 F-4059 - Harden v5 unsubscribe reject and sub/unsub property cleanup --- src/mqtt_client.c | 28 +++++++++++++ src/mqtt_packet.c | 63 ++++++++++++++++++++-------- tests/test_broker_connect.c | 56 +++++++++++++++++++++++++ tests/test_mqtt_packet.c | 82 +++++++++++++++++++++++++++++++++++++ wolfmqtt/mqtt_types.h | 4 ++ 5 files changed, 217 insertions(+), 16 deletions(-) diff --git a/src/mqtt_client.c b/src/mqtt_client.c index f5bc6fac9..6a1aca208 100644 --- a/src/mqtt_client.c +++ b/src/mqtt_client.c @@ -2506,6 +2506,10 @@ int MqttClient_Subscribe(MqttClient *client, MqttSubscribe *subscribe) int MqttClient_Unsubscribe(MqttClient *client, MqttUnsubscribe *unsubscribe) { int rc; +#ifdef WOLFMQTT_V5 + int i; + word16 reason_count; +#endif /* Validate required arguments */ if (client == NULL || unsubscribe == NULL) { @@ -2595,6 +2599,25 @@ int MqttClient_Unsubscribe(MqttClient *client, MqttUnsubscribe *unsubscribe) #endif #ifdef WOLFMQTT_V5 + /* Detect broker rejection. A v5 UNSUBACK carries one reason code per + * topic filter; any code with the high bit set (>= 0x80) means the + * broker refused to remove that subscription, so the caller must not + * assume the filter is gone. Mirrors the SUBSCRIBE rejection path. */ + if (rc == MQTT_CODE_SUCCESS && + unsubscribe->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5 && + unsubscribe->ack.reason_codes != NULL) { + reason_count = unsubscribe->ack.reason_code_count; + if (reason_count > (word16)unsubscribe->topic_count) { + reason_count = (word16)unsubscribe->topic_count; + } + for (i = 0; i < (int)reason_count; i++) { + if (unsubscribe->ack.reason_codes[i] & 0x80) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_UNSUBSCRIBE_REJECTED); + break; + } + } + } + if (unsubscribe->ack.props != NULL) { /* Release the allocated properties */ MqttClient_PropsFree(unsubscribe->ack.props); @@ -2891,6 +2914,11 @@ int MqttClient_Auth(MqttClient *client, MqttAuth* auth) } #endif + /* Scrub the decoded AUTH response from rx_buf. Its properties (e.g. the + * MQTT_PROP_AUTH_DATA SASL blob) point into rx_buf and linger there until + * the next read overwrites them, so zero the buffer before returning. */ + CLIENT_FORCE_ZERO(client->rx_buf, client->rx_buf_len); + /* reset state */ auth->stat.write = MQTT_MSG_BEGIN; diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index 66eba6ce9..672a593b7 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -2425,26 +2425,30 @@ int MqttDecode_Subscribe(byte *rx_buf, int rx_buf_len, MqttSubscribe *subscribe) byte options; word16 filter_len = 0; if (subscribe->topic_count >= MAX_MQTT_TOPICS) { - return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + tmp = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + break; } topic = &subscribe->topics[subscribe->topic_count]; tmp = MqttDecode_String(rx_payload, &topic->topic_filter, &filter_len, (word32)(rx_end - rx_payload)); if (tmp < 0) { - return tmp; + break; } if (rx_payload + tmp > rx_end) { - return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + tmp = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + break; } /* [MQTT-4.7.3-1] / [MQTT-4.7.1-2] / [MQTT-4.7.1-3] Reject * empty filters and malformed wildcard placement. */ if (!MqttPacket_TopicFilterValid(topic->topic_filter, filter_len)) { - return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + tmp = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + break; } rx_payload += tmp; if (rx_payload >= rx_end) { - return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + tmp = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + break; } options = *rx_payload++; /* MQTT 3.1.1 section 3.8.3.1: bits 2-7 of the SUBSCRIBE options byte @@ -2461,7 +2465,8 @@ int MqttDecode_Subscribe(byte *rx_buf, int rx_buf_len, MqttSubscribe *subscribe) if ((options & 0xC0) != 0 || (options & 0x03) > MQTT_QOS_2 || ((options >> 4) & 0x03) == 0x03) { - return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + tmp = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + break; } } else @@ -2469,7 +2474,8 @@ int MqttDecode_Subscribe(byte *rx_buf, int rx_buf_len, MqttSubscribe *subscribe) { if ((options & 0xFC) != 0 || (options & 0x03) > MQTT_QOS_2) { - return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + tmp = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + break; } } topic->qos = (MqttQoS)(options & 0x03); @@ -2479,8 +2485,18 @@ int MqttDecode_Subscribe(byte *rx_buf, int rx_buf_len, MqttSubscribe *subscribe) /* [MQTT-3.8.3-3] The payload of a SUBSCRIBE packet MUST contain at * least one Topic Filter / QoS pair. v5 section 3.8.3 carries the same * minimum-cardinality requirement. */ - if (subscribe->topic_count == 0) { - return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + if (tmp >= 0 && subscribe->topic_count == 0) { + tmp = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } + if (tmp < 0) { + #ifdef WOLFMQTT_V5 + /* Free any v5 properties decoded above so a malformed topic + * filter cannot leak the property list; the broker caller only + * frees props on the success path. */ + MqttProps_Free(subscribe->props); + subscribe->props = NULL; + #endif + return tmp; } } @@ -2783,23 +2799,26 @@ int MqttDecode_Unsubscribe(byte *rx_buf, int rx_buf_len, MqttUnsubscribe *unsubs MqttTopic *topic; word16 filter_len = 0; if (unsubscribe->topic_count >= MAX_MQTT_TOPICS) { - return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + tmp = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + break; } topic = &unsubscribe->topics[unsubscribe->topic_count]; tmp = MqttDecode_String(rx_payload, &topic->topic_filter, &filter_len, (word32)(rx_end - rx_payload)); if (tmp < 0) { - return tmp; + break; } if (rx_payload + tmp > rx_end) { - return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + tmp = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + break; } /* [MQTT-4.7.3-1] / [MQTT-4.7.1-2] / [MQTT-4.7.1-3]: an * UNSUBSCRIBE Topic Filter must obey the same syntax rules * as a SUBSCRIBE Topic Filter. */ if (!MqttPacket_TopicFilterValid(topic->topic_filter, filter_len)) { - return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + tmp = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + break; } rx_payload += tmp; unsubscribe->topic_count++; @@ -2808,8 +2827,18 @@ int MqttDecode_Unsubscribe(byte *rx_buf, int rx_buf_len, MqttUnsubscribe *unsubs /* [MQTT-3.10.3-2] The Payload of an UNSUBSCRIBE packet MUST * contain at least one Topic Filter. v5 section 3.10.3 carries the same * minimum-cardinality requirement. */ - if (unsubscribe->topic_count == 0) { - return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + if (tmp >= 0 && unsubscribe->topic_count == 0) { + tmp = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } + if (tmp < 0) { + #ifdef WOLFMQTT_V5 + /* Free any v5 properties decoded above so a malformed topic + * filter cannot leak the property list; the broker caller only + * frees props on the success path. */ + MqttProps_Free(unsubscribe->props); + unsubscribe->props = NULL; + #endif + return tmp; } } @@ -2888,8 +2917,10 @@ int MqttDecode_UnsubscribeAck(byte *rx_buf, int rx_buf_len, } } - /* Reason codes are stored in the payload */ + /* Reason codes are stored in the payload, one per topic filter */ unsubscribe_ack->reason_codes = rx_payload; + unsubscribe_ack->reason_code_count = + (word16)((rx_buf + header_len + remain_len) - rx_payload); } #endif } diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index ef3f6dd03..1e193719e 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -1562,6 +1562,61 @@ TEST(broker_unhandled_packet_type_closes) MqttBroker_Free(&broker); } +/* [MQTT-3.1.0-1]: a client's first packet MUST be CONNECT. The broker's + * pre-dispatch guard closes any client that sends another packet type + * first. A PUBLISH from a never-connected client must be dropped and the + * client closed - it must NOT fan out to subscribers. A deletion of the + * guard would let BrokerHandle_Publish run on the unauthenticated client, + * so the subscriber receiving zero PUBLISH packets is the load-bearing + * assertion. */ +TEST(broker_publish_before_connect_closes) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + /* Subscriber: CONNECT then SUBSCRIBE to "t" (qos 0). */ + static const byte sub_connect[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x01, 'S' + }; + static const byte sub_subscribe[] = { + 0x82, 0x06, + 0x00, 0x01, /* packet_id */ + 0x00, 0x01, 't', + 0x00 + }; + /* Attacker: PUBLISH "t"/"x" as the very first packet, no CONNECT. */ + static const byte pub_no_connect[] = { + 0x30, 0x04, + 0x00, 0x01, 't', + 'x' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(2); + mock_client_input_append(0, sub_connect, sizeof(sub_connect)); + mock_client_input_append(0, sub_subscribe, sizeof(sub_subscribe)); + mock_client_input_append(1, pub_no_connect, sizeof(pub_no_connect)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + /* Attacker connection closed for violating [MQTT-3.1.0-1]. */ + ASSERT_TRUE(g_clients[1].closed); + /* Subscriber must have received no fan-out from the pre-CONNECT PUBLISH. */ + ASSERT_EQ(0, count_packets_of_type(g_clients[0].out_buf, + g_clients[0].out_len, MQTT_PACKET_TYPE_PUBLISH)); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} + /* [MQTT-2.3.1-1] / [MQTT-4.13]: a SUBSCRIBE packet with Packet * Identifier = 0 is malformed and the broker MUST close the connection. * MqttDecode_Subscribe returns MQTT_CODE_ERROR_PACKET_ID; this test @@ -2446,6 +2501,7 @@ int main(int argc, char** argv) #endif RUN_TEST(disconnect_invalid_fixed_header_flags_fires_will); RUN_TEST(broker_unhandled_packet_type_closes); + RUN_TEST(broker_publish_before_connect_closes); RUN_TEST(broker_subscribe_packet_id_zero_closes); RUN_TEST(connack_session_present_set_on_resumed_session); RUN_TEST(connack_session_present_set_on_takeover); diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index f873e94ea..bdc6153e0 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -3052,6 +3052,34 @@ TEST(decode_subscribe_v5_empty_payload_rejected) rc = MqttDecode_Subscribe(rx_buf, (int)sizeof(rx_buf), &sub); ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); } + +/* A v5 SUBSCRIBE whose Properties block decodes cleanly but whose topic + * filter is malformed must not leak the decoded property list. The decoder + * fails on the bad filter after allocating the User Property; without the + * cleanup the broker caller returns before freeing props. Catches the + * structural invariant: sub.props == NULL on error. */ +TEST(decode_subscribe_v5_props_freed_on_bad_filter) +{ + byte rx_buf[] = { + 0x82, 0x0F, /* SUBSCRIBE, remain_len = 15 */ + 0x00, 0x01, /* packet_id */ + 0x07, /* props_len VBI = 7 */ + 0x26, 0x00, 0x01, 'k', 0x00, 0x01, 'v', /* User Property k=v */ + 0x00, 0x02, 'a', '#', /* bad filter "a#" */ + 0x00 /* options */ + }; + MqttSubscribe sub; + MqttTopic topic_arr[1]; + int rc; + + XMEMSET(&sub, 0, sizeof(sub)); + XMEMSET(topic_arr, 0, sizeof(topic_arr)); + sub.topics = topic_arr; + sub.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_Subscribe(rx_buf, (int)sizeof(rx_buf), &sub); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_NULL(sub.props); +} #endif /* WOLFMQTT_V5 */ /* [MQTT-4.7.3-1] / [MQTT-4.7.1-2] / [MQTT-4.7.1-3] Topic Filter syntax @@ -3368,6 +3396,31 @@ TEST(decode_unsubscribe_v5_empty_payload_rejected) rc = MqttDecode_Unsubscribe(rx_buf, (int)sizeof(rx_buf), &unsub); ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); } + +/* A v5 UNSUBSCRIBE whose Properties block decodes cleanly but whose topic + * filter is malformed must not leak the decoded property list. Mirrors the + * SUBSCRIBE case: catches the invariant unsub.props == NULL on error. */ +TEST(decode_unsubscribe_v5_props_freed_on_bad_filter) +{ + byte rx_buf[] = { + 0xA2, 0x0E, /* UNSUBSCRIBE, remain_len = 14 */ + 0x00, 0x01, /* packet_id */ + 0x07, /* props_len VBI = 7 */ + 0x26, 0x00, 0x01, 'k', 0x00, 0x01, 'v', /* User Property k=v */ + 0x00, 0x02, 'a', '#' /* bad filter "a#" */ + }; + MqttUnsubscribe unsub; + MqttTopic topic_arr[1]; + int rc; + + XMEMSET(&unsub, 0, sizeof(unsub)); + XMEMSET(topic_arr, 0, sizeof(topic_arr)); + unsub.topics = topic_arr; + unsub.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_Unsubscribe(rx_buf, (int)sizeof(rx_buf), &unsub); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_NULL(unsub.props); +} #endif /* WOLFMQTT_V5 */ /* UNSUBSCRIBE shares the same Topic Filter syntax rules as SUBSCRIBE @@ -3930,6 +3983,32 @@ TEST(decode_unsuback_malformed_remain_len_one) ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); } +#ifdef WOLFMQTT_V5 +/* A v5 UNSUBACK carries one reason code per topic filter after the + * properties block. The decoder must expose the count and bytes so the + * client can detect a rejection (high-bit code), mirroring SUBSCRIBE. */ +TEST(decode_unsuback_v5_reason_codes) +{ + byte buf[] = { + 0xB0, 0x05, /* UNSUBACK, remain_len = 5 */ + 0x00, 0x01, /* packet_id */ + 0x00, /* props_len = 0 */ + 0x00, 0x87 /* success, NOT_AUTHORIZED */ + }; + MqttUnsubscribeAck ack; + int rc; + + XMEMSET(&ack, 0, sizeof(ack)); + ack.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_UnsubscribeAck(buf, (int)sizeof(buf), &ack); + ASSERT_TRUE(rc > 0); + ASSERT_EQ(2, ack.reason_code_count); + ASSERT_TRUE(ack.reason_codes != NULL); + ASSERT_EQ(0x00, ack.reason_codes[0]); + ASSERT_EQ(0x87, ack.reason_codes[1]); +} +#endif /* WOLFMQTT_V5 */ + /* ============================================================================ * MqttDecode_Ping (PINGRESP) and MqttDecode_Disconnect length validation * @@ -4710,6 +4789,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_subscribe_empty_payload_rejected); #ifdef WOLFMQTT_V5 RUN_TEST(decode_subscribe_v5_empty_payload_rejected); + RUN_TEST(decode_subscribe_v5_props_freed_on_bad_filter); #endif #ifdef WOLFMQTT_V5 RUN_TEST(decode_subscribe_v5_options_byte_qos_extracted); @@ -4726,6 +4806,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_unsubscribe_bad_plus_placement_rejected); #ifdef WOLFMQTT_V5 RUN_TEST(decode_unsubscribe_v5_empty_payload_rejected); + RUN_TEST(decode_unsubscribe_v5_props_freed_on_bad_filter); #endif #endif @@ -4772,6 +4853,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_unsuback_valid); RUN_TEST(decode_unsuback_malformed_remain_len_zero); RUN_TEST(decode_unsuback_malformed_remain_len_one); + RUN_TEST(decode_unsuback_v5_reason_codes); /* MqttDecode_Ping (PINGRESP) length validation */ RUN_TEST(decode_pingresp_valid); diff --git a/wolfmqtt/mqtt_types.h b/wolfmqtt/mqtt_types.h index 1ce8a94b5..cfcc4a73e 100644 --- a/wolfmqtt/mqtt_types.h +++ b/wolfmqtt/mqtt_types.h @@ -211,6 +211,10 @@ enum MqttPacketResponseCodes { see each MqttTopic.return_code for the per-filter result. */ + MQTT_CODE_ERROR_UNSUBSCRIBE_REJECTED = -20, /* Broker rejected one or more + topic filters in an + UNSUBSCRIBE; see each reason + code in MqttUnsubscribeAck. */ MQTT_CODE_CONTINUE = -101, MQTT_CODE_STDIN_WAKE = -102, From 60d20f6f76489f885e9f5cbe9cf2fc9b1176da39 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 13:45:36 -0700 Subject: [PATCH 02/40] F-4244 F-4307 - Defer WebSocket close during publish fan-out and snapshot sub iterator --- src/mqtt_broker.c | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index d917a68c5..fe3ad5b0e 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -1105,6 +1105,7 @@ static int BrokerWsNetWrite(void* context, const byte* buf, int buf_len, BrokerClient* bc = (BrokerClient*)context; BrokerWsCtx* ws; int attempts = 0; + int prev_processing; (void)timeout_ms; @@ -1132,12 +1133,19 @@ static int BrokerWsNetWrite(void* context, const byte* buf, int buf_len, XMEMCPY(ws->tx_pending + LWS_PRE, buf, buf_len); ws->tx_len = (size_t)buf_len; - /* Request writable callback and service until data is flushed */ + /* Request writable callback and service until data is flushed. Mark this + * context busy across the spin so a peer-initiated LWS_CALLBACK_CLOSED for + * this wsi (e.g. a subscriber whose peer closes during publish fan-out) + * takes the deferred-remove path instead of freeing ws and bc out from + * under this function. Mirrors the publisher guard in BrokerClient_Process. */ lws_callback_on_writable((struct lws*)ws->wsi); + prev_processing = ws->processing; + ws->processing = 1; while (ws->tx_pending != NULL && ws->status > 0 && attempts < 100) { lws_service(lws_get_context((struct lws*)ws->wsi), 0); attempts++; } + ws->processing = prev_processing; if (ws->tx_pending != NULL) { /* Data was not flushed - connection may be in bad state */ @@ -4935,6 +4943,7 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, int i; #else BrokerSub* sub = broker->subs; + BrokerSub* next_sub = NULL; #endif /* Fan out to matching subscribers */ #ifdef WOLFMQTT_STATIC_MEMORY @@ -4943,6 +4952,11 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, if (!sub->in_use) continue; #else while (sub) { + /* Snapshot the successor before any MqttPacket_Write: a fan-out + * write can drive an lws_service spin that frees this client's + * BrokerSub nodes re-entrantly (LWS_CALLBACK_CLOSED), so reading + * sub->next afterwards would dereference a freed node. */ + next_sub = sub->next; #endif if (sub->client != NULL && sub->client->protocol_level != 0 && @@ -5056,7 +5070,7 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, } } } - sub = sub->next; + sub = next_sub; #endif } } From 57cfd36c2c6e3dbef1eabbcf25da4c857e70bf48 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 13:50:53 -0700 Subject: [PATCH 03/40] F-4246 F-4247 - Reject v5 empty topic without alias and zero subscription identifier --- src/mqtt_packet.c | 25 ++++++++++++++++++++++ tests/test_mqtt_packet.c | 45 ++++++++++++++++++++++++++++++++++------ 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index 672a593b7..40c155061 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -964,6 +964,12 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf, buf += tmp; total += tmp; prop_len -= (word32)tmp; + /* [MQTT-3.8.2.1.2] A Subscription Identifier of 0 is + * reserved and a Protocol Error. */ + if (cur_prop->type == MQTT_PROP_SUBSCRIPTION_ID && + cur_prop->data_int == 0) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } break; } case MQTT_DATA_TYPE_BINARY: @@ -1990,6 +1996,8 @@ int MqttDecode_Publish(byte *rx_buf, int rx_buf_len, MqttPublish *publish) if (publish->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { word32 props_len = 0; int tmp; + MqttProp* prop_iter; + byte has_topic_alias = 0; /* Decode Length of Properties */ if (rx_buf_len < (rx_payload - rx_buf)) { @@ -2015,6 +2023,23 @@ int MqttDecode_Publish(byte *rx_buf, int rx_buf_len, MqttPublish *publish) else { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); } + + /* [MQTT-3.3.2-8] v5 section 3.3.2.3.4: a zero-length Topic Name is + * only valid when a Topic Alias property supplies the topic. */ + if (publish->topic_name_len == 0) { + for (prop_iter = publish->props; prop_iter != NULL; + prop_iter = prop_iter->next) { + if (prop_iter->type == MQTT_PROP_TOPIC_ALIAS) { + has_topic_alias = 1; + break; + } + } + if (!has_topic_alias) { + MqttProps_Free(publish->props); + publish->props = NULL; + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } + } } #endif diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index bdc6153e0..f7e837e38 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -1209,12 +1209,12 @@ TEST(decode_publish_topic_contains_u0000_rejected) } #ifdef WOLFMQTT_V5 -/* MQTT v5 section 3.3.2.3.4: a zero-length Topic Name is permitted (paired - * with a Topic Alias property at the application layer). Wire shape: - * PUBLISH | QoS 0, remain=4, topic_len=0, props_len=0, payload "x". */ -TEST(decode_publish_v5_empty_topic_accepted) +/* MQTT v5 section 3.3.2.3.4: a zero-length Topic Name is permitted only when + * paired with a Topic Alias property. Wire: PUBLISH QoS 0, remain=7, + * topic_len=0, props_len=3, TOPIC_ALIAS(35)=1, payload "x". */ +TEST(decode_publish_v5_empty_topic_with_alias_accepted) { - byte buf[] = { 0x30, 0x04, 0x00, 0x00, 0x00, 'x' }; + byte buf[] = { 0x30, 0x07, 0x00, 0x00, 0x03, 0x23, 0x00, 0x01, 'x' }; MqttPublish pub; int rc; @@ -1223,6 +1223,37 @@ TEST(decode_publish_v5_empty_topic_accepted) rc = MqttDecode_Publish(buf, (int)sizeof(buf), &pub); ASSERT_TRUE(rc > 0); ASSERT_EQ(0, pub.topic_name_len); + MqttProps_Free(pub.props); +} + +/* [MQTT-3.3.2-8] A zero-length Topic Name with no Topic Alias property is a + * Protocol Error. Wire: PUBLISH QoS 0, remain=4, topic_len=0, props_len=0. */ +TEST(decode_publish_v5_empty_topic_no_alias_rejected) +{ + byte buf[] = { 0x30, 0x04, 0x00, 0x00, 0x00, 'x' }; + MqttPublish pub; + int rc; + + XMEMSET(&pub, 0, sizeof(pub)); + pub.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_Publish(buf, (int)sizeof(buf), &pub); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_NULL(pub.props); +} + +/* [MQTT-3.8.2.1.2] A Subscription Identifier of 0 is reserved and a Protocol + * Error. Wire: PUBLISH QoS 0, topic "t", props_len=2, SUBSCRIPTION_ID(11)=0. */ +TEST(decode_publish_v5_subscription_id_zero_rejected) +{ + byte buf[] = { 0x30, 0x07, 0x00, 0x01, 't', 0x02, 0x0B, 0x00, 'x' }; + MqttPublish pub; + int rc; + + XMEMSET(&pub, 0, sizeof(pub)); + pub.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_Publish(buf, (int)sizeof(buf), &pub); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_NULL(pub.props); } #endif /* WOLFMQTT_V5 */ @@ -4680,7 +4711,9 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_publish_wildcard_plus_topic_rejected); RUN_TEST(decode_publish_topic_contains_u0000_rejected); #ifdef WOLFMQTT_V5 - RUN_TEST(decode_publish_v5_empty_topic_accepted); + RUN_TEST(decode_publish_v5_empty_topic_with_alias_accepted); + RUN_TEST(decode_publish_v5_empty_topic_no_alias_rejected); + RUN_TEST(decode_publish_v5_subscription_id_zero_rejected); #endif RUN_TEST(decode_publish_qos1_packet_id_zero_rejected); RUN_TEST(decode_publish_qos2_packet_id_zero_rejected); From 1ef1d06f36f4ec9f990411a3a54c82950f9de454 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 13:55:07 -0700 Subject: [PATCH 04/40] F-4245 F-4249 - Reject overlong static topics and clamp v5 will delay interval --- src/mqtt_broker.c | 27 ++++++++++++++++++++++++--- wolfmqtt/mqtt_broker.h | 6 ++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index fe3ad5b0e..e7616018c 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -3685,7 +3685,13 @@ static void BrokerClient_PublishWill(MqttBroker* broker, BrokerClient* bc) BrokerClient_ClearWill(bc); return; /* will deferred, not published now */ } - /* If add failed (out of slots), publish immediately as fallback */ + /* Out of pending-will slots: fall back to immediate publication, but + * surface it - a silent fallback lets slot exhaustion erase the Will + * Delay grace window invisibly to the operator. */ + WBLOG_ERR(broker, + "broker: pending-will pool full, publishing LWT immediately " + "(delay=%u lost) sock=%d", (unsigned)bc->will_delay_sec, + (int)bc->sock); } WBLOG_DBG(broker, "broker: LWT publish sock=%d topic=%s len=%u", @@ -4336,7 +4342,15 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, MqttProp* prop = BrokerProps_Find(mc.lwt_msg->props, MQTT_PROP_WILL_DELAY_INTERVAL); if (prop != NULL) { - bc->will_delay_sec = prop->data_int; + /* Clamp to a sane maximum so a client advertising a huge + * delay (e.g. UINT32_MAX) cannot monopolize a pending-will + * slot indefinitely. */ + if (prop->data_int > BROKER_MAX_WILL_DELAY_SEC) { + bc->will_delay_sec = BROKER_MAX_WILL_DELAY_SEC; + } + else { + bc->will_delay_sec = prop->data_int; + } } } #endif @@ -4880,7 +4894,14 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, #ifdef WOLFMQTT_STATIC_MEMORY word16 tlen = pub.topic_name_len; if (tlen >= BROKER_MAX_TOPIC_LEN) { - tlen = BROKER_MAX_TOPIC_LEN - 1; + /* Reject rather than truncate: a truncated topic can match a + * different subscriber filter than the wire topic (filter/auth + * bypass) and collide retained-message keys. */ + WBLOG_ERR(broker, + "broker: PUBLISH topic too long len=%u max=%d sock=%d", + (unsigned)tlen, BROKER_MAX_TOPIC_LEN, (int)bc->sock); + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + goto publish_cleanup; } XMEMCPY(topic_buf, pub.topic_name, tlen); topic_buf[tlen] = '\0'; diff --git a/wolfmqtt/mqtt_broker.h b/wolfmqtt/mqtt_broker.h index 316e1427e..63d3b233f 100644 --- a/wolfmqtt/mqtt_broker.h +++ b/wolfmqtt/mqtt_broker.h @@ -120,6 +120,12 @@ #ifndef BROKER_MAX_PENDING_WILLS #define BROKER_MAX_PENDING_WILLS 4 #endif +/* Upper bound (seconds) the broker accepts for a v5 Will Delay Interval. + * Caps how long a deferred-will slot can be monopolized so a few clients + * advertising near-UINT32_MAX delays cannot permanently exhaust the pool. */ +#ifndef BROKER_MAX_WILL_DELAY_SEC + #define BROKER_MAX_WILL_DELAY_SEC 3600 +#endif /* Maximum concurrent inbound QoS 2 packet IDs awaiting PUBREL per client. * Used to dedup duplicate PUBLISHes per [MQTT-4.3.3] (Method B). 16 covers * any reasonable client; a misbehaving client that exceeds this gets a From 2581a2cd2c1beddef4102828964a3df79978b86e Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 14:11:00 -0700 Subject: [PATCH 05/40] F-4304 F-4654 F-4655 F-4722 F-4723 F-4727 F-4729 F-4776 F-4933 - Sanitize peer strings in broker log sinks --- src/mqtt_broker.c | 164 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 126 insertions(+), 38 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index e7616018c..412f464d8 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -116,6 +116,72 @@ static void MqttBroker_ForceZero(void* mem, word32 len) #define WBLOG_DBG(b, ...) WBLOG(b, BROKER_LOG_DEBUG, __VA_ARGS__) #endif +#ifndef WOLFMQTT_BROKER_NO_LOG +#define BROKER_LOG_SAN_SZ 128 /* per-string scratch size */ +#define BROKER_LOG_SAN_POOL 4 /* distinct buffers per log statement */ + +/* Sanitize a peer-controlled string (topic, filter, client_id, cert CN, ...) + * before it reaches a PRINTF log sink (CWE-117). Control bytes (< 0x20 and + * DEL 0x7f) become printable escapes so a remote peer cannot inject forged log + * lines (CR/LF) or hijack the operator terminal (ANSI ESC). The result is + * returned from a small rotating pool of static buffers so several sanitized + * arguments can appear in one log statement; the broker log path is single + * threaded (as is PRINTF itself). Output is NUL-terminated and truncated to + * fit. NULL src yields "(null)". */ +static const char* BrokerLog_Sanitize(const char* src) +{ + static const char hex_digits[] = "0123456789abcdef"; + static char pool[BROKER_LOG_SAN_POOL][BROKER_LOG_SAN_SZ]; + static int pool_idx = 0; + char* dst = pool[pool_idx]; + word32 di = 0; + + pool_idx = (pool_idx + 1) % BROKER_LOG_SAN_POOL; + + if (src == NULL) { + src = "(null)"; + } + + while (*src != '\0') { + byte c = (byte)*src++; + char rep[4]; + word32 repLen = 0; + word32 j; + + switch (c) { + case '\r': rep[0] = '\\'; rep[1] = 'r'; repLen = 2; break; + case '\n': rep[0] = '\\'; rep[1] = 'n'; repLen = 2; break; + case '\t': rep[0] = '\\'; rep[1] = 't'; repLen = 2; break; + case '\v': rep[0] = '\\'; rep[1] = 'v'; repLen = 2; break; + case 0x1b: rep[0] = '\\'; rep[1] = 'e'; repLen = 2; break; + default: + if (c < 0x20 || c == 0x7f) { + rep[0] = '\\'; + rep[1] = 'x'; + rep[2] = hex_digits[(c >> 4) & 0x0f]; + rep[3] = hex_digits[c & 0x0f]; + repLen = 4; + } + else { + rep[0] = (char)c; + repLen = 1; + } + break; + } + + if (di + repLen + 1 > BROKER_LOG_SAN_SZ) { + break; + } + for (j = 0; j < repLen; j++) { + dst[di++] = rep[j]; + } + } + + dst[di] = '\0'; + return dst; +} +#endif /* !WOLFMQTT_BROKER_NO_LOG */ + /* Buffer size accessors - unify static/dynamic code paths */ #ifdef WOLFMQTT_STATIC_MEMORY #define BROKER_CLIENT_TX_SZ(bc) BROKER_TX_BUF_SZ @@ -1687,7 +1753,7 @@ static void BrokerClient_DrainOutQueue(BrokerClient* bc) if (enc_rc <= 0) { WBLOG_ERR(bc->broker, "broker: drain encode failed sock=%d topic=%s rc=%d", - (int)bc->sock, cur->topic, enc_rc); + (int)bc->sock, BrokerLog_Sanitize(cur->topic), enc_rc); /* Drop just this entry and continue. Encoding failure for * a single message is not fatal to the connection. */ if (prev == NULL) { @@ -1733,13 +1799,13 @@ static void BrokerClient_DrainOutQueue(BrokerClient* bc) * stop the drain here. */ WBLOG_ERR(bc->broker, "broker: drain write failed sock=%d topic=%s rc=%d", - (int)bc->sock, cur->topic, wr_rc); + (int)bc->sock, BrokerLog_Sanitize(cur->topic), wr_rc); return; } } WBLOG_DBG(bc->broker, "broker: drain send sock=%d topic=%s qos=%d packet_id=%u dup=%d", - (int)bc->sock, cur->topic, (int)cur->qos, + (int)bc->sock, BrokerLog_Sanitize(cur->topic), (int)cur->qos, (unsigned)cur->packet_id, (int)cur->retransmit_dup); cur->retransmit_dup = 0; @@ -2223,8 +2289,8 @@ static int BrokerOrphan_EvictOldest(MqttBroker* broker) } WBLOG_INFO(broker, "broker: evicting oldest orphan client_id=%s (cap reached)", - BROKER_STR_VALID(oldest->client_id) ? oldest->client_id - : "(null)"); + BrokerLog_Sanitize(BROKER_STR_VALID(oldest->client_id) + ? oldest->client_id : "(null)")); BrokerOrphan_DropFull(broker, oldest); return 1; } @@ -2351,7 +2417,7 @@ static BrokerOrphanSession* BrokerOrphan_Take(MqttBroker* broker, broker->orphan_session_count++; WBLOG_INFO(broker, "broker: orphan session created client_id=%s queued=%d", - o->client_id, o->out_q_count); + BrokerLog_Sanitize(o->client_id), o->out_q_count); #ifdef WOLFMQTT_BROKER_PERSIST /* Re-persist the session record stamped with the orphan time so the v5 @@ -2431,12 +2497,12 @@ static int BrokerOrphan_Reclaim(MqttBroker* broker, BrokerClient* new_bc) if (retx > 0) { WBLOG_INFO(broker, "broker: orphan reclaim queued retransmit=%d client_id=%s", - retx, new_bc->client_id); + retx, BrokerLog_Sanitize(new_bc->client_id)); } } WBLOG_INFO(broker, "broker: orphan reclaimed client_id=%s queued=%d", - new_bc->client_id, new_bc->out_q_count); + BrokerLog_Sanitize(new_bc->client_id), new_bc->out_q_count); #ifdef WOLFMQTT_BROKER_PERSIST /* The reclaimed queue is now in a LIVE BrokerClient. Persisted * records for this client_id are no longer authoritative - the @@ -2497,7 +2563,8 @@ static void BrokerOrphan_Enqueue(MqttBroker* broker, BrokerOrphanSession* o, if (e == NULL) { WBLOG_ERR(broker, "broker: orphan enqueue alloc failed client_id=%s", - BROKER_STR_VALID(o->client_id) ? o->client_id : "(null)"); + BrokerLog_Sanitize( + BROKER_STR_VALID(o->client_id) ? o->client_id : "(null)")); return; } e->qos = qos; @@ -2520,8 +2587,9 @@ static void BrokerOrphan_Enqueue(MqttBroker* broker, BrokerOrphanSession* o, #endif WBLOG_DBG(broker, "broker: orphan enqueue client_id=%s topic=%s qos=%d count=%d", - BROKER_STR_VALID(o->client_id) ? o->client_id : "(null)", - topic, (int)qos, o->out_q_count); + BrokerLog_Sanitize( + BROKER_STR_VALID(o->client_id) ? o->client_id : "(null)"), + BrokerLog_Sanitize(topic), (int)qos, o->out_q_count); } /* Free every orphan (used by MqttBroker_Free and by wipe paths). */ @@ -2593,7 +2661,8 @@ static void BrokerSubs_OrphanClient(MqttBroker* broker, BrokerClient* bc) if (BrokerOrphan_Take(broker, bc) == NULL) { WBLOG_ERR(broker, "broker: orphan take failed client_id=%s - removing %d subs", - BROKER_STR_VALID(bc->client_id) ? bc->client_id : "(null)", + BrokerLog_Sanitize( + BROKER_STR_VALID(bc->client_id) ? bc->client_id : "(null)"), count); BrokerSubs_RemoveClient(broker, bc); return; @@ -2620,7 +2689,8 @@ static void BrokerSubs_OrphanClient(MqttBroker* broker, BrokerClient* bc) #endif WBLOG_INFO(broker, "broker: orphaned %d subs for client_id=%s (session persist)", - count, BROKER_STR_VALID(bc->client_id) ? bc->client_id : "(null)"); + count, BrokerLog_Sanitize( + BROKER_STR_VALID(bc->client_id) ? bc->client_id : "(null)")); } static void BrokerSubs_RemoveClient(MqttBroker* broker, BrokerClient* bc) @@ -2698,7 +2768,7 @@ static int BrokerSubs_Add(MqttBroker* broker, BrokerClient* bc, XMEMCMP(broker->subs[i].filter, filter, filter_len) == 0) { broker->subs[i].qos = qos; WBLOG_INFO(broker, "broker: sub update sock=%d filter=%s qos=%d", - (int)bc->sock, broker->subs[i].filter, qos); + (int)bc->sock, BrokerLog_Sanitize(broker->subs[i].filter), qos); return MQTT_CODE_SUCCESS; } } @@ -2709,7 +2779,7 @@ static int BrokerSubs_Add(MqttBroker* broker, BrokerClient* bc, XMEMCMP(cur->filter, filter, filter_len) == 0) { cur->qos = qos; WBLOG_INFO(broker, "broker: sub update sock=%d filter=%s qos=%d", - (int)bc->sock, cur->filter, qos); + (int)bc->sock, BrokerLog_Sanitize(cur->filter), qos); return MQTT_CODE_SUCCESS; } cur = cur->next; @@ -2783,7 +2853,7 @@ static int BrokerSubs_Add(MqttBroker* broker, BrokerClient* bc, } #endif WBLOG_INFO(broker, "broker: sub add sock=%d filter=%s qos=%d", - (int)bc->sock, sub->filter, qos); + (int)bc->sock, BrokerLog_Sanitize(sub->filter), qos); } return rc; } @@ -2806,7 +2876,7 @@ static void BrokerSubs_Remove(MqttBroker* broker, BrokerClient* bc, (word16)XSTRLEN(s->filter) == filter_len && XMEMCMP(s->filter, filter, filter_len) == 0) { WBLOG_INFO(broker, "broker: sub remove sock=%d filter=%s", - (int)bc->sock, s->filter); + (int)bc->sock, BrokerLog_Sanitize(s->filter)); XMEMSET(s, 0, sizeof(BrokerSub)); return; } @@ -2826,7 +2896,7 @@ static void BrokerSubs_Remove(MqttBroker* broker, BrokerClient* bc, broker->subs = next; } WBLOG_INFO(broker, "broker: sub remove sock=%d filter=%s", - (int)bc->sock, cur->filter); + (int)bc->sock, BrokerLog_Sanitize(cur->filter)); WOLFMQTT_FREE(cur->filter); if (cur->client_id) { WOLFMQTT_FREE(cur->client_id); @@ -3017,7 +3087,7 @@ static int BrokerSubs_ReassociateClient(MqttBroker* broker, #endif if (count > 0) { WBLOG_INFO(broker, "broker: reassociated %d subs for client_id=%s", - count, client_id); + count, BrokerLog_Sanitize(client_id)); } return count; } @@ -3153,7 +3223,8 @@ static int BrokerRetained_Store(MqttBroker* broker, const char* topic, msg->qos = qos; WBLOG_DBG(broker, "broker: retained store topic=%s len=%u qos=%d " "expiry=%u", - topic, (unsigned)payload_len, (int)qos, (unsigned)expiry_sec); + BrokerLog_Sanitize(topic), (unsigned)payload_len, (int)qos, + (unsigned)expiry_sec); #ifdef WOLFMQTT_BROKER_PERSIST (void)BrokerPersist_PutRetained(broker, msg); #endif @@ -3178,7 +3249,8 @@ static void BrokerRetained_Delete(MqttBroker* broker, const char* topic) for (i = 0; i < BROKER_MAX_RETAINED; i++) { if (broker->retained[i].in_use && XSTRCMP(broker->retained[i].topic, topic) == 0) { - WBLOG_DBG(broker, "broker: retained delete topic=%s", topic); + WBLOG_DBG(broker, "broker: retained delete topic=%s", + BrokerLog_Sanitize(topic)); XMEMSET(&broker->retained[i], 0, sizeof(BrokerRetainedMsg)); found = 1; break; @@ -3189,7 +3261,8 @@ static void BrokerRetained_Delete(MqttBroker* broker, const char* topic) while (cur) { BrokerRetainedMsg* next = cur->next; if (cur->topic != NULL && XSTRCMP(cur->topic, topic) == 0) { - WBLOG_DBG(broker, "broker: retained delete topic=%s", topic); + WBLOG_DBG(broker, "broker: retained delete topic=%s", + BrokerLog_Sanitize(topic)); if (prev) { prev->next = next; } @@ -3393,7 +3466,8 @@ static int BrokerPendingWill_Add(MqttBroker* broker, BrokerClient* bc) pw->retain = bc->will_retain; pw->publish_time = now + (WOLFMQTT_BROKER_TIME_T)bc->will_delay_sec; WBLOG_DBG(broker, "broker: will deferred sock=%d client_id=%s delay=%u", - (int)bc->sock, bc->client_id, (unsigned)bc->will_delay_sec); + (int)bc->sock, BrokerLog_Sanitize(bc->client_id), + (unsigned)bc->will_delay_sec); } return rc; } @@ -3416,7 +3490,8 @@ static void BrokerPendingWill_Cancel(MqttBroker* broker, for (i = 0; i < BROKER_MAX_PENDING_WILLS; i++) { if (broker->pending_wills[i].in_use && XSTRCMP(broker->pending_wills[i].client_id, client_id) == 0) { - WBLOG_DBG(broker, "broker: will cancelled client_id=%s", client_id); + WBLOG_DBG(broker, "broker: will cancelled client_id=%s", + BrokerLog_Sanitize(client_id)); XMEMSET(&broker->pending_wills[i], 0, sizeof(BrokerPendingWill)); return; @@ -3428,7 +3503,8 @@ static void BrokerPendingWill_Cancel(MqttBroker* broker, BrokerPendingWill* next = pw->next; if (pw->client_id != NULL && XSTRCMP(pw->client_id, client_id) == 0) { - WBLOG_DBG(broker, "broker: will cancelled client_id=%s", client_id); + WBLOG_DBG(broker, "broker: will cancelled client_id=%s", + BrokerLog_Sanitize(client_id)); if (prev) { prev->next = next; } @@ -3512,7 +3588,8 @@ static int BrokerPendingWill_Process(MqttBroker* broker) } if (now >= pw->publish_time) { WBLOG_DBG(broker, "broker: LWT deferred publish client_id=%s topic=%s " - "len=%u", pw->client_id, pw->topic, + "len=%u", BrokerLog_Sanitize(pw->client_id), + BrokerLog_Sanitize(pw->topic), (unsigned)pw->payload_len); BrokerClient_PublishWillImmediate(broker, pw->topic, pw->payload, pw->payload_len, pw->qos, pw->retain); @@ -3526,7 +3603,8 @@ static int BrokerPendingWill_Process(MqttBroker* broker) BrokerPendingWill* next = pw->next; if (now >= pw->publish_time) { WBLOG_DBG(broker, "broker: LWT deferred publish client_id=%s topic=%s " - "len=%u", pw->client_id, pw->topic, + "len=%u", BrokerLog_Sanitize(pw->client_id), + BrokerLog_Sanitize(pw->topic), (unsigned)pw->payload_len); BrokerClient_PublishWillImmediate(broker, pw->topic, pw->payload, pw->payload_len, pw->qos, pw->retain); @@ -3585,7 +3663,8 @@ static void BrokerRetained_DeliverToClient(MqttBroker* broker, /* Skip expired messages */ if (rm->expiry_sec > 0 && (now - rm->store_time) >= rm->expiry_sec) { - WBLOG_DBG(broker, "broker: retained expired topic=%s", rm->topic); + WBLOG_DBG(broker, "broker: retained expired topic=%s", + BrokerLog_Sanitize(rm->topic)); XMEMSET(rm, 0, sizeof(BrokerRetainedMsg)); continue; } @@ -3610,7 +3689,8 @@ static void BrokerRetained_DeliverToClient(MqttBroker* broker, BROKER_CLIENT_TX_SZ(bc), &out_pub, 0); if (enc_rc > 0) { WBLOG_DBG(broker, "broker: retained deliver sock=%d topic=%s " - "len=%u qos=%d", (int)bc->sock, rm->topic, + "len=%u qos=%d", (int)bc->sock, + BrokerLog_Sanitize(rm->topic), (unsigned)rm->payload_len, (int)eff_qos); (void)MqttPacket_Write(&bc->client, bc->tx_buf, enc_rc); } @@ -3623,7 +3703,8 @@ static void BrokerRetained_DeliverToClient(MqttBroker* broker, /* Skip and remove expired messages */ if (rm->expiry_sec > 0 && (now - rm->store_time) >= rm->expiry_sec) { - WBLOG_DBG(broker, "broker: retained expired topic=%s", rm->topic); + WBLOG_DBG(broker, "broker: retained expired topic=%s", + BrokerLog_Sanitize(rm->topic)); if (rm_prev) { rm_prev->next = rm_next; } @@ -3657,7 +3738,8 @@ static void BrokerRetained_DeliverToClient(MqttBroker* broker, BROKER_CLIENT_TX_SZ(bc), &out_pub, 0); if (enc_rc > 0) { WBLOG_DBG(broker, "broker: retained deliver sock=%d topic=%s " - "len=%u qos=%d", (int)bc->sock, rm->topic, + "len=%u qos=%d", (int)bc->sock, + BrokerLog_Sanitize(rm->topic), (unsigned)rm->payload_len, (int)eff_qos); (void)MqttPacket_Write(&bc->client, bc->tx_buf, enc_rc); } @@ -3695,7 +3777,8 @@ static void BrokerClient_PublishWill(MqttBroker* broker, BrokerClient* bc) } WBLOG_DBG(broker, "broker: LWT publish sock=%d topic=%s len=%u", - (int)bc->sock, bc->will_topic, (unsigned)bc->will_payload_len); + (int)bc->sock, BrokerLog_Sanitize(bc->will_topic), + (unsigned)bc->will_payload_len); BrokerClient_PublishWillImmediate(broker, bc->will_topic, bc->will_payload, bc->will_payload_len, bc->will_qos, @@ -4210,7 +4293,8 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, WBLOG_INFO(broker, "broker: CONNECT proto=%u clean=%d will=%d client_id=%s", mc.protocol_level, mc.clean_session, mc.enable_lwt, - BROKER_STR_VALID(bc->client_id) ? bc->client_id : "(null)"); + BrokerLog_Sanitize( + BROKER_STR_VALID(bc->client_id) ? bc->client_id : "(null)")); /* Client ID uniqueness and clean session handling */ bc->clean_session = mc.clean_session; @@ -4224,7 +4308,8 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, old = BrokerClient_FindByClientId(broker, bc->client_id, bc); if (old != NULL) { WBLOG_INFO(broker, "broker: duplicate client_id=%s, disconnecting " - "old sock=%d", bc->client_id, (int)old->sock); + "old sock=%d", BrokerLog_Sanitize(bc->client_id), + (int)old->sock); /* Publish old client's will on takeover */ #ifdef WOLFMQTT_V5 if (old->protocol_level < MQTT_CONNECT_PROTOCOL_LEVEL_5) { @@ -4356,7 +4441,7 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, #endif bc->has_will = 1; WBLOG_DBG(broker, "broker: LWT stored sock=%d topic=%s qos=%d retain=%d " - "len=%u delay=%u", (int)bc->sock, bc->will_topic, + "len=%u delay=%u", (int)bc->sock, BrokerLog_Sanitize(bc->will_topic), bc->will_qos, bc->will_retain, (unsigned)bc->will_payload_len, (unsigned)bc->will_delay_sec); @@ -5016,7 +5101,8 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, "broker: PUBLISH fwd sock=%d -> sock=%d " "topic=%s qos=%d len=%u", (int)bc->sock, (int)sub->client->sock, - topic, eff_qos, (unsigned)pub.total_len); + BrokerLog_Sanitize(topic), eff_qos, + (unsigned)pub.total_len); (void)MqttPacket_Write(&sub->client->client, sub->client->tx_buf, sub_rc); } @@ -5061,7 +5147,8 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, "broker: PUBLISH enq sock=%d -> sock=%d " "topic=%s qos=%d len=%u", (int)bc->sock, (int)sub->client->sock, - topic, eff_qos, (unsigned)pub.total_len); + BrokerLog_Sanitize(topic), eff_qos, + (unsigned)pub.total_len); BrokerClient_DrainOutQueue(sub->client); } } @@ -5279,7 +5366,8 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) char* cn = wolfSSL_X509_get_subjectCN(peer); (void)cn; /* may be unused if logging disabled */ WBLOG_INFO(broker, "broker: TLS client cert sock=%d CN=%s", - (int)bc->sock, cn ? cn : "(unknown)"); + (int)bc->sock, + BrokerLog_Sanitize(cn ? cn : "(unknown)")); wolfSSL_X509_free(peer); } } From d46da084225e06f8b619f1009e40f81596a751eb Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 14:29:46 -0700 Subject: [PATCH 06/40] F-4305 F-4652 - Validate broker CONNECT credentials before any session-state mutation --- src/mqtt_broker.c | 239 ++++++++++++++++++------------------ tests/test_broker_connect.c | 59 +++++++++ 2 files changed, 181 insertions(+), 117 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 412f464d8..b608a108c 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -4299,6 +4299,122 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, /* Client ID uniqueness and clean session handling */ bc->clean_session = mc.clean_session; + /* Validate credentials BEFORE any session-state mutation. Storing the + * username/password and running the credential gate here ensures an + * unauthenticated peer that guesses a client_id cannot disconnect the + * victim, fire its LWT, or hijack/destroy its persisted subscriptions: + * the duplicate-takeover and orphan-reassociation logic below only runs + * once auth has passed. */ +#ifdef WOLFMQTT_BROKER_AUTH +#ifdef WOLFMQTT_STATIC_MEMORY + bc->username[0] = '\0'; + bc->password[0] = '\0'; +#endif + bc->password_len = 0; + if (mc.username) { + word16 ulen = 0; + if (MqttDecode_Num((byte*)mc.username - MQTT_DATA_LEN_SIZE, + &ulen, MQTT_DATA_LEN_SIZE) == MQTT_DATA_LEN_SIZE) { + #ifdef WOLFMQTT_STATIC_MEMORY + if (ulen >= BROKER_MAX_USERNAME_LEN) { + WBLOG_ERR(broker, + "broker: username too long (%u >= %d) sock=%d", + (unsigned)ulen, BROKER_MAX_USERNAME_LEN, + (int)bc->sock); + #ifdef WOLFMQTT_V5 + if (mc.protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { + ack.return_code = MQTT_REASON_BAD_USER_OR_PASS; + } + else + #endif + { + ack.return_code = + MQTT_CONNECT_ACK_CODE_REFUSED_BAD_USER_PWD; + } + goto send_connack; + } + #endif + BROKER_STORE_STR_SENSITIVE(bc->username, mc.username, ulen, + BROKER_MAX_USERNAME_LEN); + } + } + if (mc.password) { + word16 plen = 0; + if (MqttDecode_Num((byte*)mc.password - MQTT_DATA_LEN_SIZE, + &plen, MQTT_DATA_LEN_SIZE) == MQTT_DATA_LEN_SIZE) { + #ifdef WOLFMQTT_STATIC_MEMORY + if (plen >= BROKER_MAX_PASSWORD_LEN) { + WBLOG_ERR(broker, + "broker: password too long (%u >= %d) sock=%d", + (unsigned)plen, BROKER_MAX_PASSWORD_LEN, + (int)bc->sock); + #ifdef WOLFMQTT_V5 + if (mc.protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { + ack.return_code = MQTT_REASON_BAD_USER_OR_PASS; + } + else + #endif + { + ack.return_code = + MQTT_CONNECT_ACK_CODE_REFUSED_BAD_USER_PWD; + } + goto send_connack; + } + #endif + /* [MQTT-3.1.3.5] Password is Binary Data and may legally + * contain 0x00. The binary-sensitive store records the + * actual length in bc->password_len so wipe and compare + * paths don't fall back to XSTRLEN truncation. */ + BROKER_STORE_BIN_SENSITIVE(bc->password, bc->password_len, + mc.password, plen, BROKER_MAX_PASSWORD_LEN); + } + } + if (broker->auth_user || broker->auth_pass) { + int auth_ok = 1; + if (broker->auth_user && ( + #ifndef WOLFMQTT_STATIC_MEMORY + bc->username == NULL || + #endif + bc->username[0] == '\0' || + BrokerStrCompare(broker->auth_user, bc->username, + BROKER_MAX_USERNAME_LEN) != 0)) { + auth_ok = 0; + } + if (broker->auth_pass && ( + #ifndef WOLFMQTT_STATIC_MEMORY + bc->password == NULL || + #endif + bc->password_len == 0 || + BrokerBufCompare((const byte*)broker->auth_pass, + (int)XSTRLEN(broker->auth_pass), + (const byte*)bc->password, (int)bc->password_len, + BROKER_MAX_PASSWORD_LEN) != 0)) { + auth_ok = 0; + } + if (!auth_ok) { + WBLOG_ERR(broker, "broker: auth failed sock=%d user=%s", + (int)bc->sock, + #ifdef WOLFMQTT_STATIC_MEMORY + BrokerLog_Sanitize(bc->username[0] ? bc->username : "(null)")); + #else + BrokerLog_Sanitize((bc->username && bc->username[0]) + ? bc->username : "(null)")); + #endif + #ifdef WOLFMQTT_V5 + if (mc.protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { + ack.return_code = MQTT_REASON_BAD_USER_OR_PASS; + } + else + #endif + { + ack.return_code = + MQTT_CONNECT_ACK_CODE_REFUSED_BAD_USER_PWD; + } + goto send_connack; + } + } +#endif /* WOLFMQTT_BROKER_AUTH */ + if (BROKER_STR_VALID(bc->client_id)) { BrokerClient* old; @@ -4448,129 +4564,18 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, } #endif /* WOLFMQTT_BROKER_WILL */ - /* Store credentials */ -#ifdef WOLFMQTT_BROKER_AUTH -#ifdef WOLFMQTT_STATIC_MEMORY - bc->username[0] = '\0'; - bc->password[0] = '\0'; -#endif - bc->password_len = 0; - if (mc.username) { - word16 ulen = 0; - if (MqttDecode_Num((byte*)mc.username - MQTT_DATA_LEN_SIZE, - &ulen, MQTT_DATA_LEN_SIZE) == MQTT_DATA_LEN_SIZE) { - #ifdef WOLFMQTT_STATIC_MEMORY - if (ulen >= BROKER_MAX_USERNAME_LEN) { - WBLOG_ERR(broker, - "broker: username too long (%u >= %d) sock=%d", - (unsigned)ulen, BROKER_MAX_USERNAME_LEN, - (int)bc->sock); - #ifdef WOLFMQTT_V5 - if (mc.protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { - ack.return_code = MQTT_REASON_BAD_USER_OR_PASS; - } - else - #endif - { - ack.return_code = - MQTT_CONNECT_ACK_CODE_REFUSED_BAD_USER_PWD; - } - goto send_connack; - } - #endif - BROKER_STORE_STR_SENSITIVE(bc->username, mc.username, ulen, - BROKER_MAX_USERNAME_LEN); - } - } - if (mc.password) { - word16 plen = 0; - if (MqttDecode_Num((byte*)mc.password - MQTT_DATA_LEN_SIZE, - &plen, MQTT_DATA_LEN_SIZE) == MQTT_DATA_LEN_SIZE) { - #ifdef WOLFMQTT_STATIC_MEMORY - if (plen >= BROKER_MAX_PASSWORD_LEN) { - WBLOG_ERR(broker, - "broker: password too long (%u >= %d) sock=%d", - (unsigned)plen, BROKER_MAX_PASSWORD_LEN, - (int)bc->sock); - #ifdef WOLFMQTT_V5 - if (mc.protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { - ack.return_code = MQTT_REASON_BAD_USER_OR_PASS; - } - else - #endif - { - ack.return_code = - MQTT_CONNECT_ACK_CODE_REFUSED_BAD_USER_PWD; - } - goto send_connack; - } - #endif - /* [MQTT-3.1.3.5] Password is Binary Data and may legally - * contain 0x00. The binary-sensitive store records the - * actual length in bc->password_len so wipe and compare - * paths don't fall back to XSTRLEN truncation. */ - BROKER_STORE_BIN_SENSITIVE(bc->password, bc->password_len, - mc.password, plen, BROKER_MAX_PASSWORD_LEN); - } - } -#endif /* WOLFMQTT_BROKER_AUTH */ - - /* Check auth before sending CONNACK. [MQTT-3.2.2-2]: when the - * accepted CleanSession=0 connection finds stored session state, - * Session Present MUST be 1; otherwise it MUST be 0. The flag is - * cleared again below for any path that overrides return_code to a - * non-zero refusal - [MQTT-3.2.2-4] requires Session Present=0 on a - * refused CONNACK. */ + /* Credentials were already validated above, before any session-state + * mutation. [MQTT-3.2.2-2]: when the accepted CleanSession=0 connection + * finds stored session state, Session Present MUST be 1; otherwise it + * MUST be 0. The flag is cleared again below for any path that overrides + * return_code to a non-zero refusal - [MQTT-3.2.2-4] requires Session + * Present=0 on a refused CONNACK. */ ack.flags = session_present ? MQTT_CONNECT_ACK_FLAG_SESSION_PRESENT : 0; ack.return_code = MQTT_CONNECT_ACK_CODE_ACCEPTED; #ifdef WOLFMQTT_V5 ack.props = NULL; #endif -#ifdef WOLFMQTT_BROKER_AUTH - if (broker->auth_user || broker->auth_pass) { - int auth_ok = 1; - if (broker->auth_user && ( - #ifndef WOLFMQTT_STATIC_MEMORY - bc->username == NULL || - #endif - bc->username[0] == '\0' || - BrokerStrCompare(broker->auth_user, bc->username, - BROKER_MAX_USERNAME_LEN) != 0)) { - auth_ok = 0; - } - if (broker->auth_pass && ( - #ifndef WOLFMQTT_STATIC_MEMORY - bc->password == NULL || - #endif - bc->password_len == 0 || - BrokerBufCompare((const byte*)broker->auth_pass, - (int)XSTRLEN(broker->auth_pass), - (const byte*)bc->password, (int)bc->password_len, - BROKER_MAX_PASSWORD_LEN) != 0)) { - auth_ok = 0; - } - if (!auth_ok) { - WBLOG_ERR(broker, "broker: auth failed sock=%d user=%s", (int)bc->sock, - #ifdef WOLFMQTT_STATIC_MEMORY - bc->username[0] ? bc->username : "(null)"); - #else - (bc->username && bc->username[0]) ? bc->username : "(null)"); - #endif - #ifdef WOLFMQTT_V5 - if (mc.protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { - ack.return_code = MQTT_REASON_BAD_USER_OR_PASS; - } - else - #endif - { - ack.return_code = - MQTT_CONNECT_ACK_CODE_REFUSED_BAD_USER_PWD; - } - } - } -#endif /* WOLFMQTT_BROKER_AUTH */ - #ifdef WOLFMQTT_V5 if (bc->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5 && ack.return_code == MQTT_CONNECT_ACK_CODE_ACCEPTED) { diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index 1e193719e..30cbabc0d 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -589,6 +589,64 @@ TEST(connect_v311_binary_password_exact_match_accepted) MqttBroker_Stop(&broker); MqttBroker_Free(&broker); } + +/* [CWE-863/CWE-639] An unauthenticated CONNECT must not mutate another + * client's session. A victim authenticates and stays connected; an attacker + * then reuses the victim's client_id with a wrong password. The broker must + * reject the attacker at the credential gate BEFORE the duplicate-takeover + * path, so the victim is never disconnected. Pre-fix, takeover ran before + * auth and closed the victim - g_clients[0].closed is the load-bearing + * assertion. */ +TEST(connect_unauth_client_id_does_not_take_over_victim) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + /* Victim: client_id "vic", user "user", pass "pass", CleanSession=1. */ + static const byte victim[] = { + 0x10, 0x1B, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0xC2, 0x00, 0x3C, + 0x00, 0x03, 'v', 'i', 'c', + 0x00, 0x04, 'u', 's', 'e', 'r', + 0x00, 0x04, 'p', 'a', 's', 's' + }; + /* Attacker: same client_id "vic", user "user", WRONG pass "bad". */ + static const byte attacker[] = { + 0x10, 0x1A, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0xC2, 0x00, 0x3C, + 0x00, 0x03, 'v', 'i', 'c', + 0x00, 0x04, 'u', 's', 'e', 'r', + 0x00, 0x03, 'b', 'a', 'd' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + broker.auth_user = "user"; + broker.auth_pass = "pass"; + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(2); + mock_client_input_append(0, victim, sizeof(victim)); + mock_client_input_append(1, attacker, sizeof(attacker)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + /* Victim authenticated and must remain connected. */ + ASSERT_TRUE(g_clients[0].out_len >= 4); + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_ACCEPTED, g_clients[0].out_buf[3]); + ASSERT_FALSE(g_clients[0].closed); + /* Attacker rejected on auth, not via session takeover. */ + ASSERT_TRUE(g_clients[1].out_len >= 4); + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_REFUSED_BAD_USER_PWD, + g_clients[1].out_buf[3]); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} #endif /* WOLFMQTT_BROKER_AUTH */ #ifdef WOLFMQTT_V5 @@ -2478,6 +2536,7 @@ int main(int argc, char** argv) #ifdef WOLFMQTT_BROKER_AUTH RUN_TEST(connect_v311_binary_password_with_embedded_nul_refused); RUN_TEST(connect_v311_binary_password_exact_match_accepted); + RUN_TEST(connect_unauth_client_id_does_not_take_over_victim); #endif #ifdef WOLFMQTT_V5 RUN_TEST(connect_v5_emptyid_assigned_id_emitted); From b4552407256833d2d977fe92ca1b9a54c93a1fd4 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 14:40:51 -0700 Subject: [PATCH 07/40] F-4308 F-4653 - Reject v5 zero topic alias and cap dynamic retained-message list --- src/mqtt_broker.c | 22 +++++++++++++--- src/mqtt_packet.c | 5 ++++ tests/test_broker_connect.c | 51 +++++++++++++++++++++++++++++++++++++ tests/test_mqtt_packet.c | 16 ++++++++++++ wolfmqtt/mqtt_broker.h | 1 + 5 files changed, 92 insertions(+), 3 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index b608a108c..de00c2129 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -3167,11 +3167,19 @@ static int BrokerRetained_Store(MqttBroker* broker, const char* topic, if (msg == NULL) { /* Allocate new node + topic */ int tlen = (int)XSTRLEN(topic); - msg = (BrokerRetainedMsg*)WOLFMQTT_MALLOC( - sizeof(BrokerRetainedMsg)); - if (msg == NULL) { + /* Cap the dynamic retained list so a client publishing RETAIN=1 to + * many distinct topics cannot grow it without bound and exhaust the + * heap; the static path is already bounded by BROKER_MAX_RETAINED. */ + if (broker->retained_count >= BROKER_MAX_RETAINED) { rc = MQTT_CODE_ERROR_MEMORY; } + if (rc == MQTT_CODE_SUCCESS) { + msg = (BrokerRetainedMsg*)WOLFMQTT_MALLOC( + sizeof(BrokerRetainedMsg)); + if (msg == NULL) { + rc = MQTT_CODE_ERROR_MEMORY; + } + } if (rc == MQTT_CODE_SUCCESS) { XMEMSET(msg, 0, sizeof(*msg)); msg->topic = (char*)WOLFMQTT_MALLOC((size_t)tlen + 1); @@ -3207,6 +3215,7 @@ static int BrokerRetained_Store(MqttBroker* broker, const char* topic, if (is_new) { msg->next = broker->retained; broker->retained = msg; + broker->retained_count++; } } else if (is_new && msg != NULL) { @@ -3274,6 +3283,9 @@ static void BrokerRetained_Delete(MqttBroker* broker, const char* topic) WOLFMQTT_FREE(cur->payload); } WOLFMQTT_FREE(cur); + if (broker->retained_count > 0) { + broker->retained_count--; + } found = 1; break; } @@ -3316,6 +3328,7 @@ static void BrokerRetained_FreeAll(MqttBroker* broker) cur = next; } broker->retained = NULL; + broker->retained_count = 0; #endif } #endif /* WOLFMQTT_BROKER_RETAINED */ @@ -3714,6 +3727,9 @@ static void BrokerRetained_DeliverToClient(MqttBroker* broker, if (rm->topic) WOLFMQTT_FREE(rm->topic); if (rm->payload) WOLFMQTT_FREE(rm->payload); WOLFMQTT_FREE(rm); + if (broker->retained_count > 0) { + broker->retained_count--; + } rm = rm_next; continue; } diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index 40c155061..580999c81 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -902,6 +902,11 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf, buf += tmp; total += tmp; prop_len -= (word32)tmp; + /* [MQTT-3.3.2-7] A Topic Alias of 0 is a Protocol Error. */ + if (cur_prop->type == MQTT_PROP_TOPIC_ALIAS && + cur_prop->data_short == 0) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } break; } case MQTT_DATA_TYPE_INT: diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index 30cbabc0d..ab5f0218b 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -1675,6 +1675,54 @@ TEST(broker_publish_before_connect_closes) MqttBroker_Free(&broker); } +#if defined(WOLFMQTT_BROKER_RETAINED) && !defined(WOLFMQTT_STATIC_MEMORY) +/* [CWE-400] The dynamic retained-message list must be bounded. A client that + * publishes RETAIN=1 to more than BROKER_MAX_RETAINED distinct topics must not + * grow the list past the cap - pre-fix it grew without bound, enabling + * heap-exhaustion DoS. */ +TEST(broker_retained_list_capped) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + byte pub[8]; + static const byte connect[] = { + 0x10, 0x0F, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x03, 'p', 'u', 'b' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(1); + mock_client_input_append(0, connect, sizeof(connect)); + /* Publish RETAIN=1 (QoS 0) to BROKER_MAX_RETAINED + 5 distinct topics. */ + for (i = 0; i < BROKER_MAX_RETAINED + 5; i++) { + pub[0] = 0x31; /* PUBLISH, retain=1 */ + pub[1] = 0x06; /* remain = 6 */ + pub[2] = 0x00; pub[3] = 0x03; /* topic len 3 */ + pub[4] = 'r'; + pub[5] = (byte)('0' + (i / 10)); + pub[6] = (byte)('0' + (i % 10)); + pub[7] = 'x'; /* payload */ + mock_client_input_append(0, pub, sizeof(pub)); + } + for (i = 0; i < BROKER_MAX_RETAINED + 12; i++) { + MqttBroker_Step(&broker); + } + + /* The list is capped, not grown to BROKER_MAX_RETAINED + 5. */ + ASSERT_EQ(BROKER_MAX_RETAINED, broker.retained_count); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* WOLFMQTT_BROKER_RETAINED && !WOLFMQTT_STATIC_MEMORY */ + /* [MQTT-2.3.1-1] / [MQTT-4.13]: a SUBSCRIBE packet with Packet * Identifier = 0 is malformed and the broker MUST close the connection. * MqttDecode_Subscribe returns MQTT_CODE_ERROR_PACKET_ID; this test @@ -2561,6 +2609,9 @@ int main(int argc, char** argv) RUN_TEST(disconnect_invalid_fixed_header_flags_fires_will); RUN_TEST(broker_unhandled_packet_type_closes); RUN_TEST(broker_publish_before_connect_closes); +#if defined(WOLFMQTT_BROKER_RETAINED) && !defined(WOLFMQTT_STATIC_MEMORY) + RUN_TEST(broker_retained_list_capped); +#endif RUN_TEST(broker_subscribe_packet_id_zero_closes); RUN_TEST(connack_session_present_set_on_resumed_session); RUN_TEST(connack_session_present_set_on_takeover); diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index f7e837e38..94ead2fff 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -1255,6 +1255,21 @@ TEST(decode_publish_v5_subscription_id_zero_rejected) ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); ASSERT_NULL(pub.props); } + +/* [MQTT-3.3.2-7] A Topic Alias of 0 is a Protocol Error. Wire: PUBLISH QoS 0, + * topic "t", props_len=3, TOPIC_ALIAS(35)=0. */ +TEST(decode_publish_v5_topic_alias_zero_rejected) +{ + byte buf[] = { 0x30, 0x08, 0x00, 0x01, 't', 0x03, 0x23, 0x00, 0x00, 'x' }; + MqttPublish pub; + int rc; + + XMEMSET(&pub, 0, sizeof(pub)); + pub.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_Publish(buf, (int)sizeof(buf), &pub); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_NULL(pub.props); +} #endif /* WOLFMQTT_V5 */ /* [MQTT-2.3.1-1] PUBLISH with QoS > 0 must carry a non-zero Packet @@ -4714,6 +4729,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_publish_v5_empty_topic_with_alias_accepted); RUN_TEST(decode_publish_v5_empty_topic_no_alias_rejected); RUN_TEST(decode_publish_v5_subscription_id_zero_rejected); + RUN_TEST(decode_publish_v5_topic_alias_zero_rejected); #endif RUN_TEST(decode_publish_qos1_packet_id_zero_rejected); RUN_TEST(decode_publish_qos2_packet_id_zero_rejected); diff --git a/wolfmqtt/mqtt_broker.h b/wolfmqtt/mqtt_broker.h index 63d3b233f..e7c9cfc64 100644 --- a/wolfmqtt/mqtt_broker.h +++ b/wolfmqtt/mqtt_broker.h @@ -637,6 +637,7 @@ typedef struct MqttBroker { BrokerSub* subs; #ifdef WOLFMQTT_BROKER_RETAINED BrokerRetainedMsg* retained; + int retained_count; #endif #ifdef WOLFMQTT_BROKER_WILL BrokerPendingWill* pending_wills; From 5b97a8dc36014c187cfb1ef53954e3892224ca9a Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 14:44:56 -0700 Subject: [PATCH 08/40] F-4529 - Add MqttClient_Subscribe broker-rejection regression test --- tests/test_mqtt_client.c | 67 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/test_mqtt_client.c b/tests/test_mqtt_client.c index 5f395c36a..cff02a95a 100644 --- a/tests/test_mqtt_client.c +++ b/tests/test_mqtt_client.c @@ -384,6 +384,72 @@ TEST(connect_clears_tx_buf_credentials) } } +/* Serves a pre-staged response packet (e.g. a SUBACK) one chunk per read so a + * full client request/response round-trip can run against the mock net. */ +static byte g_canned_buf[64]; +static int g_canned_len; +static int g_canned_pos; + +static int mock_net_read_canned(void *context, byte* buf, int buf_len, + int timeout_ms) +{ + int n; + (void)context; (void)timeout_ms; + n = g_canned_len - g_canned_pos; + if (n <= 0) { + return 0; /* exhausted -> MQTT_CODE_CONTINUE under nonblock */ + } + if (n > buf_len) { + n = buf_len; + } + XMEMCPY(buf, g_canned_buf + g_canned_pos, n); + g_canned_pos += n; + return n; +} + +/* [issue 3129] A broker that rejects a subscription returns a SUBACK whose + * per-topic return code has the high bit set (0x80 in v3.1.1, any reason + * code >= 0x80 in v5). MqttClient_Subscribe must surface this as + * MQTT_CODE_ERROR_SUBSCRIBE_REJECTED rather than MQTT_CODE_SUCCESS, else the + * application waits forever for messages on a filter the broker never + * installed. This pins the detection that previously had no test. */ +TEST(subscribe_broker_rejection_returns_subscribe_rejected) +{ + int rc; + int i; + MqttSubscribe subscribe; + MqttTopic topics[1]; + /* SUBACK v3.1.1: type=0x90, remain=3, packet_id=42, return_code=0x80. */ + static const byte suback[] = { 0x90, 0x03, 0x00, 0x2A, 0x80 }; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; + + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read_canned; + XMEMCPY(g_canned_buf, suback, sizeof(suback)); + g_canned_len = (int)sizeof(suback); + g_canned_pos = 0; + + XMEMSET(&subscribe, 0, sizeof(subscribe)); + XMEMSET(topics, 0, sizeof(topics)); + topics[0].topic_filter = "test/topic"; + topics[0].qos = MQTT_QOS_0; + subscribe.packet_id = 42; + subscribe.topic_count = 1; + subscribe.topics = topics; + + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Subscribe(&test_client, &subscribe); + } + + ASSERT_EQ(MQTT_CODE_ERROR_SUBSCRIBE_REJECTED, rc); + ASSERT_EQ(MQTT_SUBSCRIBE_ACK_CODE_FAILURE, + subscribe.topics[0].return_code); +} + /* ============================================================================ * MqttClient_Disconnect Tests * ============================================================================ */ @@ -786,6 +852,7 @@ void run_mqtt_client_tests(void) /* MqttClient_Subscribe tests */ RUN_TEST(subscribe_null_client); RUN_TEST(subscribe_null_subscribe); + RUN_TEST(subscribe_broker_rejection_returns_subscribe_rejected); /* MqttClient_Unsubscribe tests */ RUN_TEST(unsubscribe_null_client); From 337f88df616c89cbc5dd1c1fb6f2667da9ecfd17 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 14:49:37 -0700 Subject: [PATCH 09/40] F-4657 F-4658 - Validate v5 response topic and cap per-message property count --- src/mqtt_packet.c | 15 ++++++++++++ tests/test_mqtt_packet.c | 53 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index 580999c81..27e635c9a 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -832,6 +832,7 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf, { int rc = 0; int total, tmp; + int prop_count = 0; MqttProp* cur_prop; byte* buf = pbuf; @@ -839,6 +840,12 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf, while (((int)prop_len > 0) && (rc >= 0)) { + /* Bound the number of properties a single message may carry so a peer + * cannot saturate the shared property pool (CWE-770). */ + if (++prop_count > MQTT_MAX_PROPS) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PROPERTY); + break; + } /* Allocate a structure and add to head. */ cur_prop = MqttProps_Add(props); if (cur_prop == NULL) { @@ -947,6 +954,14 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf, buf += tmp; total += tmp; prop_len -= (word32)tmp; + /* [MQTT-3.3.2-14] A Response Topic is a Topic Name and + * MUST NOT contain wildcard characters. */ + if (cur_prop->type == MQTT_PROP_RESP_TOPIC && + !MqttPacket_TopicNameValid(cur_prop->data_str.str, + cur_prop->data_str.len, + MQTT_CONNECT_PROTOCOL_LEVEL_5)) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } } else { /* Invalid length */ diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index 94ead2fff..2b234a2ea 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -1270,6 +1270,57 @@ TEST(decode_publish_v5_topic_alias_zero_rejected) ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); ASSERT_NULL(pub.props); } + +/* [MQTT-3.3.2-14] A Response Topic is a Topic Name and MUST NOT contain + * wildcards. Wire: PUBLISH QoS 0, topic "t", props_len=6, RESP_TOPIC(8)="a/#". */ +TEST(decode_publish_v5_response_topic_wildcard_rejected) +{ + byte buf[] = { 0x30, 0x0B, 0x00, 0x01, 't', 0x06, + 0x08, 0x00, 0x03, 'a', '/', '#', 'x' }; + MqttPublish pub; + int rc; + + XMEMSET(&pub, 0, sizeof(pub)); + pub.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_Publish(buf, (int)sizeof(buf), &pub); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_NULL(pub.props); +} + +/* [CWE-770] A single message may not carry more than the internal + * MQTT_MAX_PROPS (default 30) properties; otherwise a peer can saturate the + * shared property pool. 40 User Property entries exceeds that cap. The cap + * returns MQTT_CODE_ERROR_PROPERTY, distinct from the MQTT_CODE_ERROR_MEMORY a + * pool-exhaustion would have produced before the fix. */ +TEST(decode_publish_v5_property_count_capped) +{ + byte buf[300]; + MqttPublish pub; + int rc, i, pos; + /* 40 User Property entries, 7 bytes each (k=v). */ + word32 nprops = 40; + word32 props_len = nprops * 7; + word32 remain = 3 + 2 + props_len; /* topic(3) + props_vbi(2) + props */ + + pos = 0; + buf[pos++] = 0x30; /* PUBLISH QoS 0 */ + buf[pos++] = (byte)((remain & 0x7F) | 0x80); /* remain VBI low */ + buf[pos++] = (byte)(remain >> 7); /* remain VBI high */ + buf[pos++] = 0x00; buf[pos++] = 0x01; buf[pos++] = 't'; /* topic "t" */ + buf[pos++] = (byte)((props_len & 0x7F) | 0x80); /* props_len VBI low */ + buf[pos++] = (byte)(props_len >> 7); /* props_len VBI high */ + for (i = 0; i < (int)nprops; i++) { + buf[pos++] = 0x26; /* User Property */ + buf[pos++] = 0x00; buf[pos++] = 0x01; buf[pos++] = 'k'; + buf[pos++] = 0x00; buf[pos++] = 0x01; buf[pos++] = 'v'; + } + + XMEMSET(&pub, 0, sizeof(pub)); + pub.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_Publish(buf, pos, &pub); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); + ASSERT_NULL(pub.props); +} #endif /* WOLFMQTT_V5 */ /* [MQTT-2.3.1-1] PUBLISH with QoS > 0 must carry a non-zero Packet @@ -4730,6 +4781,8 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_publish_v5_empty_topic_no_alias_rejected); RUN_TEST(decode_publish_v5_subscription_id_zero_rejected); RUN_TEST(decode_publish_v5_topic_alias_zero_rejected); + RUN_TEST(decode_publish_v5_response_topic_wildcard_rejected); + RUN_TEST(decode_publish_v5_property_count_capped); #endif RUN_TEST(decode_publish_qos1_packet_id_zero_rejected); RUN_TEST(decode_publish_qos2_packet_id_zero_rejected); From 2c5ce3239aaa0b8a28fcb7dedf16af7dd68cd610 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 14:55:03 -0700 Subject: [PATCH 10/40] F-4656 - Cap per-client broker subscriptions to prevent table exhaustion --- src/mqtt_broker.c | 19 +++++++++++++ tests/test_broker_connect.c | 53 +++++++++++++++++++++++++++++++++++++ wolfmqtt/mqtt_broker.h | 6 +++++ 3 files changed, 78 insertions(+) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index de00c2129..5f95afdb1 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -2732,6 +2732,7 @@ static void BrokerSubs_RemoveClient(MqttBroker* broker, BrokerClient* bc) cur = next; } #endif + bc->sub_count = 0; #ifdef WOLFMQTT_BROKER_PERSIST /* Clean-session disconnect drops the persistent record. For @@ -2786,6 +2787,14 @@ static int BrokerSubs_Add(MqttBroker* broker, BrokerClient* bc, } #endif + /* Per-client cap: prevent a single client from occupying the whole shared + * subscription table and denying SUBSCRIBE to other clients. */ + if (bc->sub_count >= BROKER_MAX_SUBS_PER_CLIENT) { + WBLOG_ERR(broker, "broker: sub cap reached sock=%d (max %d)", + (int)bc->sock, BROKER_MAX_SUBS_PER_CLIENT); + return MQTT_CODE_ERROR_MEMORY; + } + #ifdef WOLFMQTT_STATIC_MEMORY for (i = 0; i < BROKER_MAX_SUBS; i++) { if (!broker->subs[i].in_use) { @@ -2852,6 +2861,7 @@ static int BrokerSubs_Add(MqttBroker* broker, BrokerClient* bc, } } #endif + bc->sub_count++; WBLOG_INFO(broker, "broker: sub add sock=%d filter=%s qos=%d", (int)bc->sock, BrokerLog_Sanitize(sub->filter), qos); } @@ -2878,6 +2888,9 @@ static void BrokerSubs_Remove(MqttBroker* broker, BrokerClient* bc, WBLOG_INFO(broker, "broker: sub remove sock=%d filter=%s", (int)bc->sock, BrokerLog_Sanitize(s->filter)); XMEMSET(s, 0, sizeof(BrokerSub)); + if (bc->sub_count > 0) { + bc->sub_count--; + } return; } } @@ -2902,6 +2915,9 @@ static void BrokerSubs_Remove(MqttBroker* broker, BrokerClient* bc, WOLFMQTT_FREE(cur->client_id); } WOLFMQTT_FREE(cur); + if (bc->sub_count > 0) { + bc->sub_count--; + } return; } prev = cur; @@ -3086,6 +3102,9 @@ static int BrokerSubs_ReassociateClient(MqttBroker* broker, } #endif if (count > 0) { + /* The new client now owns these subs; reflect them in its cap count + * so a reconnect cannot be used to exceed BROKER_MAX_SUBS_PER_CLIENT. */ + new_bc->sub_count += count; WBLOG_INFO(broker, "broker: reassociated %d subs for client_id=%s", count, BrokerLog_Sanitize(client_id)); } diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index ab5f0218b..98670a3f6 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -1723,6 +1723,56 @@ TEST(broker_retained_list_capped) } #endif /* WOLFMQTT_BROKER_RETAINED && !WOLFMQTT_STATIC_MEMORY */ +#ifndef WOLFMQTT_STATIC_MEMORY +/* [CWE-770] A single client cannot occupy more than BROKER_MAX_SUBS_PER_CLIENT + * slots in the shared subscription table; excess SUBSCRIBEs are refused so + * other clients are not denied service. */ +TEST(broker_per_client_subscription_cap) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + byte sub[10]; + static const byte connect[] = { + 0x10, 0x0F, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x03, 's', 'u', 'b' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(1); + mock_client_input_append(0, connect, sizeof(connect)); + /* Subscribe to BROKER_MAX_SUBS_PER_CLIENT + 3 distinct filters. */ + for (i = 0; i < BROKER_MAX_SUBS_PER_CLIENT + 3; i++) { + sub[0] = 0x82; /* SUBSCRIBE */ + sub[1] = 0x08; /* remain = 8 */ + sub[2] = (byte)((i + 1) >> 8); /* packet_id hi */ + sub[3] = (byte)((i + 1) & 0xFF); /* packet_id lo */ + sub[4] = 0x00; sub[5] = 0x03; /* filter len 3 */ + sub[6] = 'f'; + sub[7] = (byte)('0' + (i / 10)); + sub[8] = (byte)('0' + (i % 10)); + sub[9] = 0x00; /* options: QoS 0 */ + mock_client_input_append(0, sub, sizeof(sub)); + } + for (i = 0; i < BROKER_MAX_SUBS_PER_CLIENT + 12; i++) { + MqttBroker_Step(&broker); + } + + /* Capped, not grown to BROKER_MAX_SUBS_PER_CLIENT + 3. */ + ASSERT_TRUE(broker.clients != NULL); + ASSERT_EQ(BROKER_MAX_SUBS_PER_CLIENT, broker.clients->sub_count); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* !WOLFMQTT_STATIC_MEMORY */ + /* [MQTT-2.3.1-1] / [MQTT-4.13]: a SUBSCRIBE packet with Packet * Identifier = 0 is malformed and the broker MUST close the connection. * MqttDecode_Subscribe returns MQTT_CODE_ERROR_PACKET_ID; this test @@ -2611,6 +2661,9 @@ int main(int argc, char** argv) RUN_TEST(broker_publish_before_connect_closes); #if defined(WOLFMQTT_BROKER_RETAINED) && !defined(WOLFMQTT_STATIC_MEMORY) RUN_TEST(broker_retained_list_capped); +#endif +#ifndef WOLFMQTT_STATIC_MEMORY + RUN_TEST(broker_per_client_subscription_cap); #endif RUN_TEST(broker_subscribe_packet_id_zero_closes); RUN_TEST(connack_session_present_set_on_resumed_session); diff --git a/wolfmqtt/mqtt_broker.h b/wolfmqtt/mqtt_broker.h index e7c9cfc64..b2c1a3238 100644 --- a/wolfmqtt/mqtt_broker.h +++ b/wolfmqtt/mqtt_broker.h @@ -93,6 +93,11 @@ #ifndef BROKER_MAX_SUBS #define BROKER_MAX_SUBS 32 #endif +/* Per-client subscription cap so one client cannot occupy the whole shared + * subscription table and deny other clients (CWE-770). */ +#ifndef BROKER_MAX_SUBS_PER_CLIENT + #define BROKER_MAX_SUBS_PER_CLIENT (BROKER_MAX_SUBS / BROKER_MAX_CLIENTS) +#endif #ifndef BROKER_MAX_CLIENT_ID_LEN #define BROKER_MAX_CLIENT_ID_LEN 64 #endif @@ -474,6 +479,7 @@ typedef struct BrokerClient { WOLFMQTT_BROKER_TIME_T last_rx; byte clean_session; byte connected; /* set after successful CONNECT handshake */ + int sub_count; /* active subscriptions owned by this client */ #ifdef WOLFMQTT_BROKER_WILL byte has_will; word16 will_payload_len; From d9e79ce83785f876f148556bd6337e58d1860ded Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 16:30:15 -0700 Subject: [PATCH 11/40] F-4928 F-4929 F-4991 - Enforce TLS peer verification in example verify callbacks --- examples/aws/awsiot.c | 15 +++++++++++---- examples/mqttexample.c | 13 +++++++++---- examples/mqttsimple/mqttsimple.c | 13 +++++++++---- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/examples/aws/awsiot.c b/examples/aws/awsiot.c index bf4c57d9c..971b8162b 100644 --- a/examples/aws/awsiot.c +++ b/examples/aws/awsiot.c @@ -291,11 +291,18 @@ static int mqtt_aws_tls_verify_cb(int preverify, WOLFSSL_X509_STORE_CTX* store) PRINTF(" Rejecting cert: verification must succeed under" " WOLFSSL_NO_ASN_STRICT"); return 0; +#elif defined(WOLFMQTT_ALLOW_INSECURE_TLS) + /* Development/testing override only: strict ASN parsing drops + * Starfield Services Root CA G2 (serialNumber=0), so the chain can + * legitimately fail here. Accept anyway to keep the demo running. + * MUST NOT be defined in production - it disables authentication. */ + PRINTF(" Allowing cert anyways (WOLFMQTT_ALLOW_INSECURE_TLS)"); #else - /* Strict ASN parsing drops Starfield Services Root CA G2 - * (serialNumber=0), so chain verification can legitimately - * fail here. Keep the demo running. */ - PRINTF(" Allowing cert anyways"); + /* Reject on any verification error by default. To run the AWS IoT + * demo against the live endpoint, build with WOLFSSL_NO_ASN_STRICT + * (loads the full trust bundle) or supply a trusted chain. */ + PRINTF(" Rejecting cert: verification failed"); + return 0; #endif } diff --git a/examples/mqttexample.c b/examples/mqttexample.c index cd667d52e..b5c7c0af5 100644 --- a/examples/mqttexample.c +++ b/examples/mqttexample.c @@ -632,13 +632,18 @@ static int mqtt_tls_verify_cb(int preverify, WOLFSSL_X509_STORE_CTX* store) wolfSSL_ERR_error_string(store->error, buffer) : "none"); PRINTF(" Subject's domain name is %s", store->domain); +#ifdef WOLFMQTT_ALLOW_INSECURE_TLS + /* Development/testing override only: accept any certificate. MUST NOT be + * defined in production builds - it disables server authentication. */ if (store->error != 0) { - /* Allowing to continue */ - /* Should check certificate and return 0 if not okay */ - PRINTF(" Allowing cert anyways"); + PRINTF(" Allowing cert anyways (WOLFMQTT_ALLOW_INSECURE_TLS)"); } - return 1; +#else + /* Propagate wolfSSL's chain-validation result so a bad certificate + * (self-signed, expired, wrong host, untrusted CA) fails the handshake. */ + return preverify; +#endif } /* Use this callback to setup TLS certificates and verify callbacks */ diff --git a/examples/mqttsimple/mqttsimple.c b/examples/mqttsimple/mqttsimple.c index 9b9769edc..ebaf00912 100644 --- a/examples/mqttsimple/mqttsimple.c +++ b/examples/mqttsimple/mqttsimple.c @@ -295,13 +295,18 @@ static int mqtt_tls_verify_cb(int preverify, WOLFSSL_X509_STORE_CTX* store) wolfSSL_ERR_error_string(store->error, buffer) : "none"); PRINTF(" Subject's domain name is %s", store->domain); +#ifdef WOLFMQTT_ALLOW_INSECURE_TLS + /* Development/testing override only: accept any certificate. MUST NOT be + * defined in production builds - it disables server authentication. */ if (store->error != 0) { - /* Allowing to continue */ - /* Should check certificate and return 0 if not okay */ - PRINTF(" Allowing cert anyways"); + PRINTF(" Allowing cert anyways (WOLFMQTT_ALLOW_INSECURE_TLS)"); } - return 1; +#else + /* Propagate wolfSSL's chain-validation result so a bad certificate + * (self-signed, expired, wrong host, untrusted CA) fails the handshake. */ + return preverify; +#endif } /* Use this callback to setup TLS certificates and verify callbacks */ From 087d9f0130d7e6b5e86ab1778f83e1dd8eacc077 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 16:34:56 -0700 Subject: [PATCH 12/40] F-5766 F-5767 F-5768 F-5769 F-5861 - Sanitize broker-controlled strings in example log output --- examples/aws/awsiot.c | 38 ++++++++++++++++++++++++------ examples/azure/azureiothub.c | 6 +++-- examples/mqttsimple/mqttsimple.c | 6 +++-- examples/multithread/multithread.c | 6 +++-- examples/wiot/wiot.c | 6 +++-- 5 files changed, 47 insertions(+), 15 deletions(-) diff --git a/examples/aws/awsiot.c b/examples/aws/awsiot.c index 971b8162b..5d1c4b547 100644 --- a/examples/aws/awsiot.c +++ b/examples/aws/awsiot.c @@ -51,6 +51,7 @@ #include "awsiot.h" #include "examples/mqttexample.h" #include "examples/mqttnet.h" +#include "examples/mqtt_log.h" #include /* Locals */ @@ -359,6 +360,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, { MQTTCtx* mqttCtx = (MQTTCtx*)client->ctx; byte buf[PRINT_BUFFER_SIZE+1]; + char safebuf[PRINT_BUFFER_SIZE+1]; word32 len; (void)mqttCtx; @@ -374,7 +376,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, /* Print incoming message */ PRINTF("MQTT Message: Topic %s, Qos %d, Len %u", - buf, msg->qos, msg->total_len); + mqtt_log_sanitize(safebuf, (word32)sizeof(safebuf), (char*)buf), msg->qos, msg->total_len); } /* Print message payload */ @@ -385,7 +387,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, XMEMCPY(buf, msg->buffer, len); buf[len] = '\0'; /* Make sure its null terminated */ PRINTF("Payload (%d - %d) printing %d bytes:" LINE_END "%s", - msg->buffer_pos, msg->buffer_pos + msg->buffer_len, len, buf); + msg->buffer_pos, msg->buffer_pos + msg->buffer_len, len, mqtt_log_sanitize(safebuf, (word32)sizeof(safebuf), (char*)buf)); if (msg_done) { PRINTF("MQTT Message: Done"); @@ -402,11 +404,30 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, /* The property callback is called after decoding a packet that contains at least one property. The property list is deallocated after returning from the callback. */ +/* Copy a length-delimited, broker-controlled property string into dst and + * sanitize it for safe logging (CWE-117). */ +static const char* awsiot_log_prop(char* dst, word32 dstLen, + const char* src, word32 srcLen) +{ + char tmp[PRINT_BUFFER_SIZE + 1]; + word32 n = srcLen; + if (n > PRINT_BUFFER_SIZE) { + n = PRINT_BUFFER_SIZE; + } + if (src != NULL && n > 0) { + XMEMCPY(tmp, src, n); + } + tmp[n] = '\0'; + return mqtt_log_sanitize(dst, dstLen, tmp); +} + static int mqtt_property_cb(MqttClient *client, MqttProp *head, void *ctx) { MqttProp *prop = head; int rc = 0; MQTTCtx* mqttCtx; + char safebuf[PRINT_BUFFER_SIZE + 1]; + char safebuf2[PRINT_BUFFER_SIZE + 1]; if ((client == NULL) || (client->ctx == NULL)) { return MQTT_CODE_ERROR_BAD_ARG; @@ -454,14 +475,17 @@ static int mqtt_property_cb(MqttClient *client, MqttProp *head, void *ctx) break; case MQTT_PROP_REASON_STR: - PRINTF("Reason String: %.*s", - prop->data_str.len, prop->data_str.str); + PRINTF("Reason String: %s", + awsiot_log_prop(safebuf, (word32)sizeof(safebuf), + prop->data_str.str, prop->data_str.len)); break; case MQTT_PROP_USER_PROP: - PRINTF("User property: key=\"%.*s\", value=\"%.*s\"", - prop->data_str.len, prop->data_str.str, - prop->data_str2.len, prop->data_str2.str); + PRINTF("User property: key=\"%s\", value=\"%s\"", + awsiot_log_prop(safebuf, (word32)sizeof(safebuf), + prop->data_str.str, prop->data_str.len), + awsiot_log_prop(safebuf2, (word32)sizeof(safebuf2), + prop->data_str2.str, prop->data_str2.len)); break; case MQTT_PROP_ASSIGNED_CLIENT_ID: diff --git a/examples/azure/azureiothub.c b/examples/azure/azureiothub.c index b80bf97e1..23491cc38 100644 --- a/examples/azure/azureiothub.c +++ b/examples/azure/azureiothub.c @@ -65,6 +65,7 @@ #include "azureiothub.h" #include "examples/mqttexample.h" #include "examples/mqttnet.h" +#include "examples/mqtt_log.h" /* Locals */ static int mStopRead = 0; @@ -134,6 +135,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, { MQTTCtx* mqttCtx = (MQTTCtx*)client->ctx; byte buf[PRINT_BUFFER_SIZE+1]; + char safebuf[PRINT_BUFFER_SIZE+1]; word32 len; (void)mqttCtx; @@ -149,7 +151,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, /* Print incoming message */ PRINTF("MQTT Message: Topic %s, Qos %d, Len %u", - buf, msg->qos, msg->total_len); + mqtt_log_sanitize(safebuf, (word32)sizeof(safebuf), (char*)buf), msg->qos, msg->total_len); } /* Print message payload */ @@ -160,7 +162,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, XMEMCPY(buf, msg->buffer, len); buf[len] = '\0'; /* Make sure its null terminated */ PRINTF("Payload (%d - %d) printing %d bytes:" LINE_END "%s", - msg->buffer_pos, msg->buffer_pos + msg->buffer_len, len, buf); + msg->buffer_pos, msg->buffer_pos + msg->buffer_len, len, mqtt_log_sanitize(safebuf, (word32)sizeof(safebuf), (char*)buf)); if (msg_done) { PRINTF("MQTT Message: Done"); diff --git a/examples/mqttsimple/mqttsimple.c b/examples/mqttsimple/mqttsimple.c index ebaf00912..ef23c0852 100644 --- a/examples/mqttsimple/mqttsimple.c +++ b/examples/mqttsimple/mqttsimple.c @@ -28,6 +28,7 @@ #include "wolfmqtt/mqtt_client.h" #include "examples/mqttport.h" +#include "examples/mqtt_log.h" #include "mqttsimple.h" /* Requires BSD Style Socket */ @@ -84,6 +85,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, byte msg_new, byte msg_done) { byte buf[PRINT_BUFFER_SIZE+1]; + char safebuf[PRINT_BUFFER_SIZE+1]; word32 len; (void)client; @@ -99,7 +101,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, /* Print incoming message */ PRINTF("MQTT Message: Topic %s, Qos %d, Len %u", - buf, msg->qos, msg->total_len); + mqtt_log_sanitize(safebuf, (word32)sizeof(safebuf), (char*)buf), msg->qos, msg->total_len); } /* Print message payload */ @@ -110,7 +112,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, XMEMCPY(buf, msg->buffer, len); buf[len] = '\0'; /* Make sure its null terminated */ PRINTF("Payload (%d - %d) printing %d bytes:" LINE_END "%s", - msg->buffer_pos, msg->buffer_pos + msg->buffer_len, len, buf); + msg->buffer_pos, msg->buffer_pos + msg->buffer_len, len, mqtt_log_sanitize(safebuf, (word32)sizeof(safebuf), (char*)buf)); if (msg_done) { PRINTF("MQTT Message: Done"); diff --git a/examples/multithread/multithread.c b/examples/multithread/multithread.c index 9901a21e5..83df6bdcc 100644 --- a/examples/multithread/multithread.c +++ b/examples/multithread/multithread.c @@ -28,6 +28,7 @@ #include "multithread.h" #include "examples/mqttnet.h" +#include "examples/mqtt_log.h" #include "examples/mqttexample.h" #include @@ -165,6 +166,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, byte msg_new, byte msg_done) { byte buf[PRINT_BUFFER_SIZE+1]; + char safebuf[PRINT_BUFFER_SIZE+1]; word32 len; MQTTCtx* mqttCtx = (MQTTCtx*)client->ctx; (void)mqttCtx; @@ -181,7 +183,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, /* Print incoming message */ PRINTF("MQTT Message: Topic %s, Qos %d, Id %d, Len %u, %u, %u", - buf, msg->qos, msg->packet_id, msg->total_len, msg->buffer_len, + mqtt_log_sanitize(safebuf, (word32)sizeof(safebuf), (char*)buf), msg->qos, msg->packet_id, msg->total_len, msg->buffer_len, msg->buffer_pos); } @@ -193,7 +195,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, XMEMCPY(buf, msg->buffer, len); buf[len] = '\0'; /* Make sure its null terminated */ PRINTF("Payload (%d - %d) printing %d bytes:" LINE_END "%s", - msg->buffer_pos, msg->buffer_pos + msg->buffer_len, len, buf); + msg->buffer_pos, msg->buffer_pos + msg->buffer_len, len, mqtt_log_sanitize(safebuf, (word32)sizeof(safebuf), (char*)buf)); if (msg_done) { /* for test mode: count the number of messages received */ diff --git a/examples/wiot/wiot.c b/examples/wiot/wiot.c index f426705a3..e23272aa9 100644 --- a/examples/wiot/wiot.c +++ b/examples/wiot/wiot.c @@ -35,6 +35,7 @@ #include "wiot.h" #include "examples/mqttexample.h" #include "examples/mqttnet.h" +#include "examples/mqtt_log.h" /* Locals */ static int mStopRead = 0; @@ -82,6 +83,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, byte msg_new, byte msg_done) { byte buf[PRINT_BUFFER_SIZE+1]; + char safebuf[PRINT_BUFFER_SIZE+1]; word32 len; MQTTCtx* mqttCtx = (MQTTCtx*)client->ctx; @@ -98,7 +100,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, /* Print incoming message */ PRINTF("MQTT Message: Topic %s, Qos %d, Len %u", - buf, msg->qos, msg->total_len); + mqtt_log_sanitize(safebuf, (word32)sizeof(safebuf), (char*)buf), msg->qos, msg->total_len); /* for test mode: check if TEST_MESSAGE was received */ if (mqttCtx->test_mode) { @@ -117,7 +119,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, XMEMCPY(buf, msg->buffer, len); buf[len] = '\0'; /* Make sure its null terminated */ PRINTF("Payload (%d - %d) printing %d bytes:" LINE_END "%s", - msg->buffer_pos, msg->buffer_pos + msg->buffer_len, len, buf); + msg->buffer_pos, msg->buffer_pos + msg->buffer_len, len, mqtt_log_sanitize(safebuf, (word32)sizeof(safebuf), (char*)buf)); if (msg_done) { PRINTF("MQTT Message: Done"); From c829dbcaa648ccb0b98661b51f9bdb6ec0cbdd35 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 16:40:23 -0700 Subject: [PATCH 13/40] F-4724 F-4772 - Fix firmware client length-overflow and topic-gate bypass --- examples/firmware/fwclient.c | 37 ++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/examples/firmware/fwclient.c b/examples/firmware/fwclient.c index d90fcb509..3c70c75c0 100644 --- a/examples/firmware/fwclient.c +++ b/examples/firmware/fwclient.c @@ -118,13 +118,31 @@ static int fw_message_process(MQTTCtx *mqttCtx, byte* buffer, word32 len) #ifdef ENABLE_FIRMWARE_SIG ecc_key eccKey; #endif - word32 check_len = sizeof(FirmwareHeader) + header->sigLen + - header->pubKeyLen + header->fwLen; - - /* Verify entire message was received */ - if (len != check_len) { + word32 remaining; + + /* Validate the field sizes sequentially against the received length. + * A summed length check (sizeof(header) + sigLen + pubKeyLen + fwLen) + * overflows word32 for attacker-chosen fwLen, letting a too-short buffer + * pass and leaving pubKeyBuf/fwBuf pointing past the allocation + * (CWE-190 -> heap OOB read/write). */ + if (len < sizeof(FirmwareHeader)) { + PRINTF("Message smaller than firmware header! %d", len); + return EXIT_FAILURE; + } + remaining = len - sizeof(FirmwareHeader); + if (header->sigLen > remaining) { + PRINTF("Firmware sigLen exceeds message! %d", header->sigLen); + return EXIT_FAILURE; + } + remaining -= header->sigLen; + if (header->pubKeyLen > remaining) { + PRINTF("Firmware pubKeyLen exceeds message! %d", header->pubKeyLen); + return EXIT_FAILURE; + } + remaining -= header->pubKeyLen; + if (header->fwLen != remaining) { PRINTF("Message header vs. actual size mismatch! %d != %d", - len, check_len); + header->fwLen, remaining); return EXIT_FAILURE; } @@ -172,10 +190,13 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, { MQTTCtx* mqttCtx = (MQTTCtx*)client->ctx; - /* Verify this message is for the firmware topic */ + /* Verify this message is for the firmware topic. Compare against the full + * expected length (not the wire-supplied topic_name_len) so a zero-length + * (v5 Topic Alias) or byte-prefix topic cannot pass the gate. */ if (msg_new && + msg->topic_name_len == (word16)XSTRLEN(mqttCtx->topic_name) && XSTRNCMP(msg->topic_name, mqttCtx->topic_name, - msg->topic_name_len) == 0 && + XSTRLEN(mqttCtx->topic_name)) == 0 && !mFwBuf) { /* Allocate buffer for entire message */ From 154bfaed936585700466c83803913816e19cebbf Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 16:43:08 -0700 Subject: [PATCH 14/40] F-5148 - Prevent broker client double-free on re-entrant takeover close --- src/mqtt_broker.c | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 5f95afdb1..9a1cce4e5 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -2159,6 +2159,7 @@ static void BrokerClient_Remove(MqttBroker* broker, BrokerClient* bc) #ifndef WOLFMQTT_STATIC_MEMORY BrokerClient* cur; BrokerClient* prev = NULL; + int found = 0; #endif if (broker == NULL || bc == NULL) { @@ -2175,13 +2176,21 @@ static void BrokerClient_Remove(MqttBroker* broker, BrokerClient* bc) else { broker->clients = cur->next; } + found = 1; break; } prev = cur; cur = cur->next; } -#endif + /* Only free when bc was actually unlinked. A re-entrant close callback + * (e.g. WebSocket LWS_CALLBACK_CLOSED during a takeover fan-out) can have + * already removed and freed bc; freeing again here would double-free. */ + if (found) { + BrokerClient_Free(bc); + } +#else BrokerClient_Free(bc); +#endif } /* -------------------------------------------------------------------------- */ @@ -4461,6 +4470,15 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, WBLOG_INFO(broker, "broker: duplicate client_id=%s, disconnecting " "old sock=%d", BrokerLog_Sanitize(bc->client_id), (int)old->sock); +#ifdef ENABLE_MQTT_WEBSOCKET + /* Guard old across its takeover fan-out: a re-entrant + * LWS_CALLBACK_CLOSED for old's dropped socket must take the + * deferred-remove path instead of freeing old mid-takeover, which + * would UAF (BrokerSubs_RemoveClient) and double-free below. */ + if (old->ws_ctx != NULL) { + ((BrokerWsCtx*)old->ws_ctx)->processing = 1; + } +#endif /* Publish old client's will on takeover */ #ifdef WOLFMQTT_V5 if (old->protocol_level < MQTT_CONNECT_PROTOCOL_LEVEL_5) { @@ -4473,6 +4491,11 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, } #else BrokerClient_PublishWill(broker, old); +#endif +#ifdef ENABLE_MQTT_WEBSOCKET + if (old->ws_ctx != NULL) { + ((BrokerWsCtx*)old->ws_ctx)->processing = 0; + } #endif if (!mc.clean_session) { /* Reassociate old client's subs to new client */ From be003afa6c9342f317a4e0f92126f08d343fba98 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 17:06:45 -0700 Subject: [PATCH 15/40] F-4773 F-4992 F-5116 F-5143 F-5771 - Fix WebSocket fan-out UAF, TLS verification, and log injection --- examples/websocket/net_libwebsockets.c | 16 +++++++++-- examples/websocket/websocket_client.c | 8 ++++-- src/mqtt_broker.c | 38 +++++++++++++++++++++++--- wolfmqtt/mqtt_broker.h | 2 ++ 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/examples/websocket/net_libwebsockets.c b/examples/websocket/net_libwebsockets.c index c005acd1a..35deefaa4 100644 --- a/examples/websocket/net_libwebsockets.c +++ b/examples/websocket/net_libwebsockets.c @@ -68,8 +68,13 @@ static int callback_mqtt(struct lws *wsi, enum lws_callback_reasons reason, else if (reason == LWS_CALLBACK_CLIENT_CONNECTION_ERROR) { net->status = -1; } - else if (reason == LWS_CALLBACK_CLOSED) { + else if (reason == LWS_CALLBACK_CLOSED || + reason == LWS_CALLBACK_CLIENT_CLOSED) { net->status = 0; + /* libwebsockets frees the wsi after this callback returns; clear the + * dangling pointer so NetWebsocket_Disconnect's `if (net->wsi)` guard + * skips lws_close_reason() on freed memory (CWE-416). */ + net->wsi = NULL; } else if (reason == LWS_CALLBACK_CLIENT_RECEIVE) { if (in && len > 0) { @@ -185,8 +190,13 @@ int NetWebsocket_Connect(void *ctx, const char* host, word16 port, /* Set SSL options for the connection if TLS is enabled */ if (mqttCtx && mqttCtx->use_tls) { conn_info.ssl_connection = LCCSCF_USE_SSL; - conn_info.ssl_connection |= LCCSCF_ALLOW_SELFSIGNED; - conn_info.ssl_connection |= LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK; + /* Only relax verification when no CA was supplied (dev/self-signed). + * When the operator provides a CA via -A, perform full chain and + * hostname verification rather than silently disabling it. */ + if (mqttCtx->ca_file == NULL) { + conn_info.ssl_connection |= LCCSCF_ALLOW_SELFSIGNED; + conn_info.ssl_connection |= LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK; + } } #endif /* ENABLE_MQTT_TLS */ diff --git a/examples/websocket/websocket_client.c b/examples/websocket/websocket_client.c index 46211a5dd..3db8d463b 100644 --- a/examples/websocket/websocket_client.c +++ b/examples/websocket/websocket_client.c @@ -27,6 +27,7 @@ #include "wolfmqtt/mqtt_client.h" #include "examples/mqttnet.h" #include "examples/mqttexample.h" +#include "examples/mqtt_log.h" #include "examples/websocket/net_libwebsockets.h" #include #include @@ -46,6 +47,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, byte msg_new, byte msg_done) { byte buf[PRINT_BUFFER_SIZE+1]; + char safebuf[PRINT_BUFFER_SIZE+1]; word32 len; (void)client; @@ -61,7 +63,8 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, /* Print topic */ printf("MQTT Message: Topic %s, Qos %d, Len %u", - buf, msg->qos, msg->total_len); + mqtt_log_sanitize(safebuf, (word32)sizeof(safebuf), (char*)buf), + msg->qos, msg->total_len); } /* Print message payload */ @@ -73,7 +76,8 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, buf[len] = '\0'; /* Make sure it's null terminated */ printf("Payload (%d - %d): %s\n", - msg->buffer_pos, msg->buffer_pos + msg->buffer_len, buf); + msg->buffer_pos, msg->buffer_pos + msg->buffer_len, + mqtt_log_sanitize(safebuf, (word32)sizeof(safebuf), (char*)buf)); if (msg_done) { printf("MQTT Message: Done\n"); diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 9a1cce4e5..bdbd702e8 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -3300,6 +3300,15 @@ static void BrokerRetained_Delete(MqttBroker* broker, const char* topic) if (cur->topic != NULL && XSTRCMP(cur->topic, topic) == 0) { WBLOG_DBG(broker, "broker: retained delete topic=%s", BrokerLog_Sanitize(topic)); + if (broker->retained_delivering > 0) { + /* A delivery loop is iterating this list (possibly re-entered + * via a WebSocket fan-out). Freeing now would invalidate that + * loop's saved next pointer (CWE-416); flag for deferred reap + * by the delivery loop instead. */ + cur->pending_delete = 1; + found = 1; + break; + } if (prev) { prev->next = next; } @@ -3695,6 +3704,13 @@ static void BrokerRetained_DeliverToClient(MqttBroker* broker, } now = WOLFMQTT_BROKER_GET_TIME_S(); +#ifndef WOLFMQTT_STATIC_MEMORY + /* Mark a delivery in progress so a re-entrant BrokerRetained_Delete (via a + * WebSocket fan-out close) defers its free instead of invalidating the + * loop's saved next pointer. */ + broker->retained_delivering++; +#endif + #ifdef WOLFMQTT_STATIC_MEMORY for (i = 0; i < BROKER_MAX_RETAINED; i++) { BrokerRetainedMsg* rm = &broker->retained[i]; @@ -3741,9 +3757,11 @@ static void BrokerRetained_DeliverToClient(MqttBroker* broker, rm = broker->retained; while (rm) { BrokerRetainedMsg* rm_next = rm->next; - /* Skip and remove expired messages */ - if (rm->expiry_sec > 0 && - (now - rm->store_time) >= rm->expiry_sec) { + /* Reap expired messages and any nodes a re-entrant delete deferred + * (pending_delete). The free here is safe: rm_next is already saved. */ + if (rm->pending_delete || + (rm->expiry_sec > 0 && + (now - rm->store_time) >= rm->expiry_sec)) { WBLOG_DBG(broker, "broker: retained expired topic=%s", BrokerLog_Sanitize(rm->topic)); if (rm_prev) { @@ -3792,6 +3810,12 @@ static void BrokerRetained_DeliverToClient(MqttBroker* broker, rm = rm_next; } #endif + +#ifndef WOLFMQTT_STATIC_MEMORY + if (broker->retained_delivering > 0) { + broker->retained_delivering--; + } +#endif } #endif /* WOLFMQTT_BROKER_RETAINED */ @@ -3839,6 +3863,7 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, int i; #else BrokerSub* sub; + BrokerSub* next_sub = NULL; #endif if (broker == NULL || topic == NULL) { @@ -3868,6 +3893,11 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, #else sub = broker->subs; while (sub) { + /* Snapshot the successor before any MqttPacket_Write: a WS fan-out + * write can drive an lws_service spin whose re-entrant CLOSED frees + * this client's BrokerSub nodes, so reading sub->next afterwards + * would dereference a freed node. */ + next_sub = sub->next; #endif if (sub->client != NULL && sub->client->protocol_level != 0 && BROKER_STR_VALID(sub->filter) && @@ -3897,7 +3927,7 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, } } #ifndef WOLFMQTT_STATIC_MEMORY - sub = sub->next; + sub = next_sub; #endif } } diff --git a/wolfmqtt/mqtt_broker.h b/wolfmqtt/mqtt_broker.h index b2c1a3238..94ec97691 100644 --- a/wolfmqtt/mqtt_broker.h +++ b/wolfmqtt/mqtt_broker.h @@ -576,6 +576,7 @@ typedef struct BrokerRetainedMsg { WOLFMQTT_BROKER_TIME_T store_time; /* when stored (seconds) */ word32 expiry_sec; /* v5 message expiry (0=none) */ MqttQoS qos; /* [MQTT-3.3.1-5] stored QoS */ + byte pending_delete; /* deferred free during delivery */ } BrokerRetainedMsg; #endif /* WOLFMQTT_BROKER_RETAINED */ @@ -644,6 +645,7 @@ typedef struct MqttBroker { #ifdef WOLFMQTT_BROKER_RETAINED BrokerRetainedMsg* retained; int retained_count; + int retained_delivering; /* re-entrancy guard for delete */ #endif #ifdef WOLFMQTT_BROKER_WILL BrokerPendingWill* pending_wills; From 61870fb87e30d332fd5a8695a5c4a87693de68eb Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 17:14:28 -0700 Subject: [PATCH 16/40] F-4726 F-5115 - Enforce WebSocket origin allowlist and TLS-only listener policy --- src/mqtt_broker.c | 29 ++++++++++++++++++++++++++++- wolfmqtt/mqtt_broker.h | 4 ++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index bdbd702e8..afc1a880c 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -1007,7 +1007,24 @@ static int callback_broker_mqtt(struct lws *wsi, (void)user; - if (reason == LWS_CALLBACK_ESTABLISHED) { + if (reason == LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION) { + /* CSWSH defense: when an Origin allowlist is configured, reject a + * browser-supplied Origin that does not match exactly. Requests with + * no Origin header (native clients) are not browser-originated and are + * allowed through. */ + if (broker != NULL && broker->ws_allowed_origin != NULL) { + char origin[256]; + int olen = lws_hdr_copy(wsi, origin, (int)sizeof(origin), + WSI_TOKEN_ORIGIN); + if (olen > 0 && + XSTRCMP(origin, broker->ws_allowed_origin) != 0) { + WBLOG_ERR(broker, "broker: ws origin rejected: %s", + BrokerLog_Sanitize(origin)); + return -1; + } + } + } + else if (reason == LWS_CALLBACK_ESTABLISHED) { bc = BrokerClient_AddWs(broker, wsi); if (bc == NULL) { WBLOG_ERR(broker, "broker: ws accept rejected (alloc)"); @@ -6059,6 +6076,16 @@ int MqttBroker_Start(MqttBroker* broker) #ifdef ENABLE_MQTT_WEBSOCKET if (broker->use_websocket) { + #ifdef WOLFMQTT_BROKER_NO_INSECURE + /* TLS-only build: a plaintext WebSocket listener would silently bypass + * the policy the plain-TCP listener enforces. Require WSS. */ + if (broker->ws_tls_cert == NULL) { + WBLOG_ERR(broker, "broker: plaintext WebSocket listener refused in " + "TLS-only build (WOLFMQTT_BROKER_NO_INSECURE); set ws_tls_cert " + "for WSS"); + return MQTT_CODE_ERROR_BAD_ARG; + } + #endif rc = BrokerWs_Init(broker); if (rc != MQTT_CODE_SUCCESS) { WBLOG_ERR(broker, "broker: WebSocket init failed rc=%d", rc); diff --git a/wolfmqtt/mqtt_broker.h b/wolfmqtt/mqtt_broker.h index 94ec97691..dfe9a88bf 100644 --- a/wolfmqtt/mqtt_broker.h +++ b/wolfmqtt/mqtt_broker.h @@ -658,6 +658,10 @@ typedef struct MqttBroker { const char *ws_tls_cert; const char *ws_tls_key; const char *ws_tls_ca; + /* Optional exact Origin allowlist for browser WebSocket connections. When + * non-NULL, a request whose HTTP Origin header is present and does not + * match is rejected (CSWSH defense). NULL = no Origin enforcement. */ + const char *ws_allowed_origin; #endif #ifdef WOLFMQTT_BROKER_PERSIST /* Pointer (not embedded struct) so the broker stays small when no From dc8ab1e09d5c9c9ce4b81f851dd5dc3a0e128e5d Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 17:20:16 -0700 Subject: [PATCH 17/40] F-4994 F-4996 F-5149 - Clamp publish payload copy, reject duplicate v5 props, fix packet-size comparison --- src/mqtt_broker.c | 1 + src/mqtt_packet.c | 40 +++++++++++++++++++++++++++++++++++++-- tests/test_mqtt_packet.c | 41 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index afc1a880c..8f00533c0 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -5197,6 +5197,7 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, out_pub.duplicate = 0; out_pub.buffer = payload; out_pub.total_len = pub.total_len; + out_pub.buffer_len = pub.buffer_len; #ifdef WOLFMQTT_V5 out_pub.protocol_level = sub->client->protocol_level; if (sub->client->protocol_level >= diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index 27e635c9a..d38ba7cc6 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -833,6 +833,7 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf, int rc = 0; int total, tmp; int prop_count = 0; + word32 seen_lo = 0, seen_hi = 0; /* singleton-property duplicate guard */ MqttProp* cur_prop; byte* buf = pbuf; @@ -880,6 +881,30 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf, break; } + /* [MQTT v5 2.2.2.2] Every property except User Property and + * Subscription Identifier MUST appear at most once; a duplicate is a + * Protocol Error. */ + if (cur_prop->type != MQTT_PROP_USER_PROP && + cur_prop->type != MQTT_PROP_SUBSCRIPTION_ID) { + word32 bit; + if (cur_prop->type < 32) { + bit = (word32)1 << cur_prop->type; + if (seen_lo & bit) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PROPERTY); + break; + } + seen_lo |= bit; + } + else { + bit = (word32)1 << (cur_prop->type - 32); + if (seen_hi & bit) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PROPERTY); + break; + } + seen_hi |= bit; + } + } + switch (gPropMatrix[cur_prop->type].data) { case MQTT_DATA_TYPE_BYTE: @@ -1922,6 +1947,14 @@ int MqttEncode_Publish(byte *tx_buf, int tx_buf_len, MqttPublish *publish, if (payload_len > (tx_buf_len - (header_len + variable_len))) { payload_len = (tx_buf_len - (header_len + variable_len)); } + /* Never copy more than the bytes actually present in publish->buffer. + * total_len carries the wire-declared size, which can exceed the + * decoded buffer_len; clamping here prevents an OOB read past the + * source buffer during fan-out. */ + if (publish->buffer_len > 0 && + payload_len > (int)publish->buffer_len) { + payload_len = (int)publish->buffer_len; + } if (tx_payload != NULL) { XMEMCPY(tx_payload, publish->buffer, payload_len); } @@ -3614,8 +3647,11 @@ int MqttPacket_Write(MqttClient *client, byte* tx_buf, int tx_buf_len) { int rc; #ifdef WOLFMQTT_V5 - if ((client->packet_sz_max > 0) && (tx_buf_len > - (int)client->packet_sz_max)) + /* Compare unsigned: packet_sz_max is word32 and a malicious CONNACK can + * set it above INT_MAX, where the old (int) cast turned it negative and + * wedged every write. */ + if ((client->packet_sz_max > 0) && (tx_buf_len > 0) && + ((word32)tx_buf_len > client->packet_sz_max)) { rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_SERVER_PROP); } diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index 2b234a2ea..6fb158cee 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -772,6 +772,28 @@ TEST(encode_publish_qos1_valid) ASSERT_TRUE(rc > 0); } +/* [CWE-125] The encoder must clamp the copied payload to buffer_len so a + * total_len larger than the bytes actually present cannot read past the + * source buffer (the broker fan-out OOB read). */ +TEST(encode_publish_clamps_payload_to_buffer_len) +{ + byte tx_buf[256]; + byte payload[100]; + MqttPublish pub; + int rc; + + XMEMSET(&pub, 0, sizeof(pub)); + XMEMSET(payload, 'x', sizeof(payload)); + pub.topic_name = "t"; + pub.qos = MQTT_QOS_0; + pub.buffer = payload; + pub.buffer_len = 10; /* only 10 valid bytes present */ + pub.total_len = 100; /* wire-declared size exceeds buffer_len */ + rc = MqttEncode_Publish(tx_buf, (int)sizeof(tx_buf), &pub, 0); + ASSERT_TRUE(rc > 0); + ASSERT_EQ(10, (int)pub.buffer_pos); +} + /* Verify the fixed-header flag bits (retain/QoS/dup) are actually emitted. * Covers deletion mutations of the retain / qos / duplicate branches in * MqttEncode_FixedHeader. */ @@ -1321,6 +1343,23 @@ TEST(decode_publish_v5_property_count_capped) ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); ASSERT_NULL(pub.props); } + +/* [MQTT v5 2.2.2.2] A singleton property (here TOPIC_ALIAS) appearing twice is + * a Protocol Error. Wire: PUBLISH QoS 0, topic "t", props_len=6, two + * TOPIC_ALIAS(35)=1 entries. */ +TEST(decode_publish_v5_duplicate_singleton_prop_rejected) +{ + byte buf[] = { 0x30, 0x0A, 0x00, 0x01, 't', 0x06, + 0x23, 0x00, 0x01, 0x23, 0x00, 0x01 }; + MqttPublish pub; + int rc; + + XMEMSET(&pub, 0, sizeof(pub)); + pub.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_Publish(buf, (int)sizeof(buf), &pub); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); + ASSERT_NULL(pub.props); +} #endif /* WOLFMQTT_V5 */ /* [MQTT-2.3.1-1] PUBLISH with QoS > 0 must carry a non-zero Packet @@ -4751,6 +4790,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(encode_publish_qos2_packet_id_zero); RUN_TEST(encode_publish_qos0_packet_id_zero_ok); RUN_TEST(encode_publish_qos1_valid); + RUN_TEST(encode_publish_clamps_payload_to_buffer_len); RUN_TEST(encode_publish_qos1_retain_flags_in_header); RUN_TEST(encode_publish_qos2_duplicate_flags_in_header); RUN_TEST(encode_publish_qos0_no_flags_in_header); @@ -4783,6 +4823,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_publish_v5_topic_alias_zero_rejected); RUN_TEST(decode_publish_v5_response_topic_wildcard_rejected); RUN_TEST(decode_publish_v5_property_count_capped); + RUN_TEST(decode_publish_v5_duplicate_singleton_prop_rejected); #endif RUN_TEST(decode_publish_qos1_packet_id_zero_rejected); RUN_TEST(decode_publish_qos2_packet_id_zero_rejected); From 356fd47431b7027bce98805bfcb9e8824885e726 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 17:25:34 -0700 Subject: [PATCH 18/40] F-4997 F-5862 F-5863 F-5865 - Broker disconnect-with-will, pending-will re-entrancy, scoped wolfSSL cleanup, fan-out write reset --- src/mqtt_broker.c | 45 ++++++++++++++++++++++++++++++++++++++++-- src/mqtt_socket.c | 14 ++++++++++++- wolfmqtt/mqtt_client.h | 3 ++- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 8f00533c0..1dc284ce5 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -3678,9 +3678,22 @@ static int BrokerPendingWill_Process(MqttBroker* broker) if (prev) { prev->next = next; } - else { + else if (broker->pending_wills == pw) { broker->pending_wills = next; } + else { + /* The fan-out above re-entered BrokerPendingWill_Add (a WS + * close), prepending a node, so pw is no longer the head. + * Unlink pw via its real predecessor instead of clobbering the + * new head with the stale saved next. */ + BrokerPendingWill* p = broker->pending_wills; + while (p != NULL && p->next != pw) { + p = p->next; + } + if (p != NULL) { + p->next = next; + } + } if (pw->client_id) WOLFMQTT_FREE(pw->client_id); if (pw->topic) { BROKER_FORCE_ZERO(pw->topic, XSTRLEN(pw->topic) + 1); @@ -5186,6 +5199,7 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, * Sub-encoder failure is logged but not propagated. */ { int sub_rc; + int wr; MqttPublish out_pub; XMEMSET(&out_pub, 0, sizeof(out_pub)); out_pub.topic_name = topic; @@ -5214,8 +5228,15 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, (int)bc->sock, (int)sub->client->sock, BrokerLog_Sanitize(topic), eff_qos, (unsigned)pub.total_len); - (void)MqttPacket_Write(&sub->client->client, + wr = MqttPacket_Write(&sub->client->client, sub->client->tx_buf, sub_rc); + /* On a partial/non-blocking write the subscriber's + * write.pos is left mid-packet; reset it so the next + * fan-out does not resume a stale offset and desync + * this subscriber's stream. */ + if (wr != sub_rc) { + sub->client->client.write.pos = 0; + } } else { WBLOG_ERR(broker, @@ -5694,6 +5715,26 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) BrokerClient_AbnormalClose(broker, bc); return 0; } + #if defined(WOLFMQTT_V5) && defined(WOLFMQTT_BROKER_WILL) + /* [MQTT-3.14.4-3] A v5 DISCONNECT with Reason Code 0x04 + * (Disconnect with Will Message) asks the broker to publish + * the Will rather than discard it. */ + if (bc->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5 && + bc->client.packet.remain_len > 0) { + MqttDisconnect disc; + XMEMSET(&disc, 0, sizeof(disc)); + disc.protocol_level = bc->protocol_level; + if (MqttDecode_Disconnect(bc->rx_buf, rc, &disc) >= 0 && + disc.reason_code == + MQTT_REASON_DISCONNECT_W_WILL_MSG) { + BrokerClient_PublishWill(broker, bc); + } + else { + BrokerClient_ClearWill(bc); + } + } + else + #endif BrokerClient_ClearWill(bc); /* normal disconnect */ /* Session persistence: keep subs if clean_session=0 */ if (bc->clean_session) { diff --git a/src/mqtt_socket.c b/src/mqtt_socket.c index b2617f4bf..e6866203c 100644 --- a/src/mqtt_socket.c +++ b/src/mqtt_socket.c @@ -435,6 +435,12 @@ int MqttSocket_Connect(MqttClient *client, const char* host, word16 port, /* Setup the WolfSSL library */ rc = wolfSSL_Init(); + if (rc == WOLFSSL_SUCCESS) { + /* Record that this client incremented wolfSSL's global init + * refcount so only it calls wolfSSL_Cleanup() on disconnect, + * never tearing down wolfSSL for sibling clients. */ + MqttClient_Flags(client, 0, MQTT_CLIENT_FLAG_WOLFSSL_INIT); + } /* Issue callback to allow setup of the wolfSSL_CTX and cert verification settings */ @@ -571,7 +577,13 @@ int MqttSocket_Disconnect(MqttClient *client) wolfSSL_CTX_free(client->tls.ctx); client->tls.ctx = NULL; } - wolfSSL_Cleanup(); + /* Only balance a wolfSSL_Init() this client actually made; otherwise a + * non-TLS disconnect could drive the global refcount to 0 and tear + * wolfSSL down for other live clients in the same process. */ + if (MqttClient_Flags(client, 0, 0) & MQTT_CLIENT_FLAG_WOLFSSL_INIT) { + wolfSSL_Cleanup(); + MqttClient_Flags(client, MQTT_CLIENT_FLAG_WOLFSSL_INIT, 0); + } #endif MqttClient_Flags(client, (MQTT_CLIENT_FLAG_IS_TLS | MQTT_CLIENT_FLAG_IS_DTLS), 0); diff --git a/wolfmqtt/mqtt_client.h b/wolfmqtt/mqtt_client.h index 416be714c..c03a04646 100644 --- a/wolfmqtt/mqtt_client.h +++ b/wolfmqtt/mqtt_client.h @@ -103,7 +103,8 @@ typedef int (*MqttPublishCb)(MqttPublish* publish); enum MqttClientFlags { MQTT_CLIENT_FLAG_IS_CONNECTED = 0x01 << 0, MQTT_CLIENT_FLAG_IS_TLS = 0x01 << 1, - MQTT_CLIENT_FLAG_IS_DTLS = 0x01 << 2 + MQTT_CLIENT_FLAG_IS_DTLS = 0x01 << 2, + MQTT_CLIENT_FLAG_WOLFSSL_INIT = 0x01 << 3 /* this client called wolfSSL_Init */ }; /*! \brief Sets flags in the MqttClient structure. To be used from the application before calling MqttClient_NetConnect. From 0154f268fd7e8791bdf05c47ded9fb2723ab5658 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 17:32:14 -0700 Subject: [PATCH 19/40] F-4777 F-4932 - Reject client PUBLISH subscription id and avoid false PUBACK on topic alloc failure --- src/mqtt_broker.c | 29 +++++++++++++++++++++--- tests/test_broker_connect.c | 44 +++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 1dc284ce5..adeb1cfd4 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -5031,6 +5031,21 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, return rc; } +#ifdef WOLFMQTT_V5 + /* [MQTT-3.3.4-6] A PUBLISH sent from a client to the server MUST NOT carry + * a Subscription Identifier; reject as a Protocol Error instead of + * forwarding the foreign id to subscribers. */ + if (bc->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5 && + pub.props != NULL && + BrokerProps_Find(pub.props, MQTT_PROP_SUBSCRIPTION_ID) != NULL) { + WBLOG_ERR(broker, "broker: client PUBLISH carried SUBSCRIPTION_ID " + "sock=%d", (int)bc->sock); + (void)BrokerSend_Disconnect(bc, MQTT_REASON_PROTOCOL_ERR); + rc = MQTT_CODE_ERROR_MALFORMED_DATA; + goto publish_cleanup; + } +#endif + /* [MQTT-3.3.2-2] PUBLISH topic name wildcard / [MQTT-4.7.3-1] * empty-topic checks now live in MqttDecode_Publish via * MqttPacket_TopicNameValid, which has already returned @@ -5116,10 +5131,18 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, topic = topic_buf; #else topic = (char*)WOLFMQTT_MALLOC(pub.topic_name_len + 1); - if (topic != NULL) { - XMEMCPY(topic, pub.topic_name, pub.topic_name_len); - topic[pub.topic_name_len] = '\0'; + if (topic == NULL) { + /* Without the topic copy, retained-store and fan-out are skipped; + * returning here prevents the QoS 1/2 ACK encoder below from + * falsely reporting SUCCESS for a message that was never + * delivered. */ + WBLOG_ERR(broker, "broker: PUBLISH topic alloc failed sock=%d", + (int)bc->sock); + rc = MQTT_CODE_ERROR_MEMORY; + goto publish_cleanup; } + XMEMCPY(topic, pub.topic_name, pub.topic_name_len); + topic[pub.topic_name_len] = '\0'; #endif } /* Use payload pointer directly from decoded packet - rx_buf is not diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index 98670a3f6..925d0e681 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -1773,6 +1773,47 @@ TEST(broker_per_client_subscription_cap) } #endif /* !WOLFMQTT_STATIC_MEMORY */ +#ifdef WOLFMQTT_V5 +/* [MQTT-3.3.4-6] A client PUBLISH carrying a Subscription Identifier is a + * Protocol Error; the broker must reject and close, not forward the foreign + * id to subscribers. */ +TEST(broker_publish_with_subscription_id_closes) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + static const byte connect[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, /* props len = 0 */ + 0x00, 0x01, 'P' + }; + static const byte publish[] = { + 0x30, 0x06, + 0x00, 0x01, 't', + 0x02, 0x0B, 0x05 /* props: SUBSCRIPTION_ID = 5 */ + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(1); + mock_client_input_append(0, connect, sizeof(connect)); + mock_client_input_append(0, publish, sizeof(publish)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + ASSERT_TRUE(g_client_closed); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* WOLFMQTT_V5 */ + /* [MQTT-2.3.1-1] / [MQTT-4.13]: a SUBSCRIBE packet with Packet * Identifier = 0 is malformed and the broker MUST close the connection. * MqttDecode_Subscribe returns MQTT_CODE_ERROR_PACKET_ID; this test @@ -2664,6 +2705,9 @@ int main(int argc, char** argv) #endif #ifndef WOLFMQTT_STATIC_MEMORY RUN_TEST(broker_per_client_subscription_cap); +#endif +#ifdef WOLFMQTT_V5 + RUN_TEST(broker_publish_with_subscription_id_closes); #endif RUN_TEST(broker_subscribe_packet_id_zero_closes); RUN_TEST(connack_session_present_set_on_resumed_session); From dcbc2c00d20e92e9124bd547f40f97a67f80c7cf Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 17:48:43 -0700 Subject: [PATCH 20/40] F-4993 F-5512 - Add broker pre-CONNECT idle timeout and scrub WebSocket credential staging buffer --- src/mqtt_broker.c | 23 +++++++++++++++++++++++ wolfmqtt/mqtt_broker.h | 6 ++++++ 2 files changed, 29 insertions(+) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index adeb1cfd4..f6074818d 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -1174,8 +1174,13 @@ static int BrokerWsNetRead(void* context, byte* buf, int buf_len, if (ret < (int)ws->rx_len) { XMEMMOVE(ws->rx_buffer, ws->rx_buffer + ret, ws->rx_len - ret); ws->rx_len -= ret; + /* Scrub the vacated tail so consumed CONNECT credentials do not + * linger in the WS staging buffer. */ + BROKER_FORCE_ZERO(ws->rx_buffer + ws->rx_len, (word32)ret); } else { + /* Scrub the whole consumed buffer (may hold username/password). */ + BROKER_FORCE_ZERO(ws->rx_buffer, (word32)ws->rx_len); ws->rx_len = 0; } @@ -1299,6 +1304,8 @@ static int BrokerWsNetDisconnect(void* context) ws->tx_pending = NULL; } ws->tx_len = 0; + /* Scrub any unconsumed staged bytes (may hold credentials) before free. */ + BROKER_FORCE_ZERO(ws->rx_buffer, (word32)sizeof(ws->rx_buffer)); ws->rx_len = 0; ws->status = 0; @@ -5818,6 +5825,22 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) return 0; } } + else if (!bc->connected) { + /* Pre-CONNECT idle timeout. A freshly accepted client has + * keep_alive_sec == 0 until CONNECT completes, so it is not covered by + * the keepalive check above; evict it once it has been idle past the + * deadline (last_rx is the accept time until the first full packet) + * so half-open sockets cannot squat the client table. */ + WOLFMQTT_BROKER_TIME_T now = WOLFMQTT_BROKER_GET_TIME_S(); + if ((now - bc->last_rx) > + (WOLFMQTT_BROKER_TIME_T)BROKER_CONNECT_TIMEOUT_SEC) { + WBLOG_ERR(broker, "broker: pre-CONNECT idle timeout sock=%d", + (int)bc->sock); + BrokerSubs_RemoveClient(broker, bc); + BrokerClient_Remove(broker, bc); + return 0; + } + } return activity; } diff --git a/wolfmqtt/mqtt_broker.h b/wolfmqtt/mqtt_broker.h index dfe9a88bf..7881aab66 100644 --- a/wolfmqtt/mqtt_broker.h +++ b/wolfmqtt/mqtt_broker.h @@ -131,6 +131,12 @@ #ifndef BROKER_MAX_WILL_DELAY_SEC #define BROKER_MAX_WILL_DELAY_SEC 3600 #endif +/* Seconds a freshly accepted client has to complete a CONNECT before the + * broker evicts it, so half-open pre-CONNECT sockets cannot exhaust the client + * table (Slowloris / slot exhaustion). */ +#ifndef BROKER_CONNECT_TIMEOUT_SEC + #define BROKER_CONNECT_TIMEOUT_SEC 10 +#endif /* Maximum concurrent inbound QoS 2 packet IDs awaiting PUBREL per client. * Used to dedup duplicate PUBLISHes per [MQTT-4.3.3] (Method B). 16 covers * any reasonable client; a misbehaving client that exceeds this gets a From ab130c2131621885ab6de5c5e3a4e6a5fbaea9c7 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 09:05:59 -0700 Subject: [PATCH 21/40] Address skoll review: bound UNSUBACK reason codes, reject oversized broker publish, enforce retained cap on persist restore --- src/mqtt_broker.c | 12 ++++++++++++ src/mqtt_broker_persist.c | 7 +++++++ src/mqtt_packet.c | 8 ++++++++ tests/test_mqtt_packet.c | 19 +++++++++++++++++++ 4 files changed, 46 insertions(+) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index f6074818d..786031a88 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -5053,6 +5053,18 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, } #endif + /* The decoder only captured pub.buffer_len bytes of the payload; if that is + * short of the declared pub.total_len the message exceeded the broker + * receive buffer. Reject it rather than fanning out a packet whose + * Remaining Length overstates the bytes we actually hold. */ + if (pub.total_len > 0 && pub.buffer_len < pub.total_len) { + WBLOG_ERR(broker, + "broker: PUBLISH payload exceeds buffer (have %u of %u) sock=%d", + (unsigned)pub.buffer_len, (unsigned)pub.total_len, (int)bc->sock); + rc = MQTT_CODE_ERROR_OUT_OF_BUFFER; + goto publish_cleanup; + } + /* [MQTT-3.3.2-2] PUBLISH topic name wildcard / [MQTT-4.7.3-1] * empty-topic checks now live in MqttDecode_Publish via * MqttPacket_TopicNameValid, which has already returned diff --git a/src/mqtt_broker_persist.c b/src/mqtt_broker_persist.c index d9e21195b..6efac31ed 100644 --- a/src/mqtt_broker_persist.c +++ b/src/mqtt_broker_persist.c @@ -1311,6 +1311,12 @@ static int wmqb_decode_and_insert_retained(MqttBroker* broker, #else { BrokerRetainedMsg* m; + /* Enforce the same dynamic cap on restore that BrokerRetained_Store + * applies, otherwise a restart would reset retained_count to 0 and + * let a client add another BROKER_MAX_RETAINED topics (heap DoS). */ + if (broker->retained_count >= BROKER_MAX_RETAINED) { + return 0; + } m = (BrokerRetainedMsg*)WOLFMQTT_MALLOC(sizeof(*m)); if (m == NULL) { return MQTT_CODE_ERROR_MEMORY; @@ -1346,6 +1352,7 @@ static int wmqb_decode_and_insert_retained(MqttBroker* broker, m->expiry_sec = expiry; m->next = broker->retained; broker->retained = m; + broker->retained_count++; } #endif return 0; diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index d38ba7cc6..7506583a5 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -2945,6 +2945,14 @@ int MqttDecode_UnsubscribeAck(byte *rx_buf, int rx_buf_len, return header_len; } + /* Reject a truncated read: a broker advertising a Remaining Length larger + * than the bytes actually received would otherwise make reason_code_count + * (derived from remain_len) point past the buffer, so the caller's + * rejection scan reads out of bounds. */ + if (rx_buf_len < header_len + remain_len) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + } + /* Validate remain_len (need at least packet_id) */ if (remain_len < MQTT_DATA_LEN_SIZE) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index 6fb158cee..b95551571 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -4119,6 +4119,24 @@ TEST(decode_unsuback_malformed_remain_len_one) ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); } +/* A broker advertising a Remaining Length larger than the bytes actually + * received must be rejected, not decoded - otherwise reason_code_count would + * span past the buffer and the client's rejection scan reads out of bounds. */ +TEST(decode_unsuback_truncated_remain_len_rejected) +{ + /* Remaining Length = 10, but only 3 bytes follow the 2-byte header. */ + byte buf[] = { 0xB0, 0x0A, 0x00, 0x01, 0x00 }; + MqttUnsubscribeAck ack; + int rc; + + XMEMSET(&ack, 0, sizeof(ack)); +#ifdef WOLFMQTT_V5 + ack.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; +#endif + rc = MqttDecode_UnsubscribeAck(buf, (int)sizeof(buf), &ack); + ASSERT_EQ(MQTT_CODE_ERROR_OUT_OF_BUFFER, rc); +} + #ifdef WOLFMQTT_V5 /* A v5 UNSUBACK carries one reason code per topic filter after the * properties block. The decoder must expose the count and bytes so the @@ -4996,6 +5014,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_unsuback_valid); RUN_TEST(decode_unsuback_malformed_remain_len_zero); RUN_TEST(decode_unsuback_malformed_remain_len_one); + RUN_TEST(decode_unsuback_truncated_remain_len_rejected); RUN_TEST(decode_unsuback_v5_reason_codes); /* MqttDecode_Ping (PINGRESP) length validation */ From 0fb17bc111f74899fed6a8e3ca8f2f66940a00e8 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 09:15:41 -0700 Subject: [PATCH 22/40] Address skoll review: guard v5-only test registrations for non-v5 builds --- tests/test_mqtt_client.c | 2 ++ tests/test_mqtt_packet.c | 2 ++ 2 files changed, 4 insertions(+) diff --git a/tests/test_mqtt_client.c b/tests/test_mqtt_client.c index cff02a95a..c35525adc 100644 --- a/tests/test_mqtt_client.c +++ b/tests/test_mqtt_client.c @@ -424,7 +424,9 @@ TEST(subscribe_broker_rejection_returns_subscribe_rejected) rc = test_init_client(); ASSERT_EQ(MQTT_CODE_SUCCESS, rc); +#ifdef WOLFMQTT_V5 test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; +#endif test_net.write = mock_net_write_accept; test_net.read = mock_net_read_canned; diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index b95551571..f3ec03fc9 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -5015,7 +5015,9 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_unsuback_malformed_remain_len_zero); RUN_TEST(decode_unsuback_malformed_remain_len_one); RUN_TEST(decode_unsuback_truncated_remain_len_rejected); +#ifdef WOLFMQTT_V5 RUN_TEST(decode_unsuback_v5_reason_codes); +#endif /* MqttDecode_Ping (PINGRESP) length validation */ RUN_TEST(decode_pingresp_valid); From 6bec2fb5ceafadaf3e8e88f85e5c6b8aa151f00b Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 09:27:05 -0700 Subject: [PATCH 23/40] Clarify MqttClient_Auth rx_buf scrub ordering (skoll FP: props consumed before scrub) --- src/mqtt_client.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/mqtt_client.c b/src/mqtt_client.c index 6a1aca208..cdc44c70b 100644 --- a/src/mqtt_client.c +++ b/src/mqtt_client.c @@ -2915,8 +2915,12 @@ int MqttClient_Auth(MqttClient *client, MqttAuth* auth) #endif /* Scrub the decoded AUTH response from rx_buf. Its properties (e.g. the - * MQTT_PROP_AUTH_DATA SASL blob) point into rx_buf and linger there until - * the next read overwrites them, so zero the buffer before returning. */ + * MQTT_PROP_AUTH_DATA SASL blob) point into rx_buf and would otherwise + * linger until the next read overwrites them. This is safe to do here: + * MqttClient_WaitType above has already run MqttClient_DecodePacket, which + * delivered auth->props to the property callback and then freed them and + * set auth->props = NULL. So the bytes are consumed before this scrub and + * the caller has no live pointer into rx_buf. */ CLIENT_FORCE_ZERO(client->rx_buf, client->rx_buf_len); /* reset state */ From 41dabde9ef9e0dde1ec35a01ceaa14958419a7a9 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 09:36:08 -0700 Subject: [PATCH 24/40] Address skoll review: free v5 DISCONNECT props, floor per-client sub cap at 1, count restore cap skips --- src/mqtt_broker.c | 4 ++++ src/mqtt_broker_persist.c | 6 ++++-- wolfmqtt/mqtt_broker.h | 8 +++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 786031a88..891b9e35d 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -5774,6 +5774,10 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) else { BrokerClient_ClearWill(bc); } + /* Free any decoded v5 DISCONNECT properties. */ + if (disc.props != NULL) { + (void)MqttProps_Free(disc.props); + } } else #endif diff --git a/src/mqtt_broker_persist.c b/src/mqtt_broker_persist.c index 6efac31ed..502945aed 100644 --- a/src/mqtt_broker_persist.c +++ b/src/mqtt_broker_persist.c @@ -1313,9 +1313,11 @@ static int wmqb_decode_and_insert_retained(MqttBroker* broker, BrokerRetainedMsg* m; /* Enforce the same dynamic cap on restore that BrokerRetained_Store * applies, otherwise a restart would reset retained_count to 0 and - * let a client add another BROKER_MAX_RETAINED topics (heap DoS). */ + * let a client add another BROKER_MAX_RETAINED topics (heap DoS). + * Return non-zero so the iterator counts it as skipped (not loaded) + * rather than silently hiding the dropped record. */ if (broker->retained_count >= BROKER_MAX_RETAINED) { - return 0; + return 1; } m = (BrokerRetainedMsg*)WOLFMQTT_MALLOC(sizeof(*m)); if (m == NULL) { diff --git a/wolfmqtt/mqtt_broker.h b/wolfmqtt/mqtt_broker.h index 7881aab66..9f04f0d67 100644 --- a/wolfmqtt/mqtt_broker.h +++ b/wolfmqtt/mqtt_broker.h @@ -96,7 +96,13 @@ /* Per-client subscription cap so one client cannot occupy the whole shared * subscription table and deny other clients (CWE-770). */ #ifndef BROKER_MAX_SUBS_PER_CLIENT - #define BROKER_MAX_SUBS_PER_CLIENT (BROKER_MAX_SUBS / BROKER_MAX_CLIENTS) + /* At least 1 so a config where BROKER_MAX_CLIENTS > BROKER_MAX_SUBS does + * not collapse the cap to 0 and reject every subscription. */ + #if (BROKER_MAX_SUBS / BROKER_MAX_CLIENTS) > 0 + #define BROKER_MAX_SUBS_PER_CLIENT (BROKER_MAX_SUBS / BROKER_MAX_CLIENTS) + #else + #define BROKER_MAX_SUBS_PER_CLIENT 1 + #endif #endif #ifndef BROKER_MAX_CLIENT_ID_LEN #define BROKER_MAX_CLIENT_ID_LEN 64 From bd8104d5856d8e8b9a60059d59ec98bc7fa39a09 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 09:48:47 -0700 Subject: [PATCH 25/40] Address skoll review: map UNSUBSCRIBE_REJECTED string and add client unsubscribe-reject test --- src/mqtt_client.c | 2 ++ tests/test_mqtt_client.c | 43 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/mqtt_client.c b/src/mqtt_client.c index cdc44c70b..8559d87f4 100644 --- a/src/mqtt_client.c +++ b/src/mqtt_client.c @@ -3194,6 +3194,8 @@ const char* MqttClient_ReturnCodeToString(int return_code) return "Error (Broker refused connection)"; case MQTT_CODE_ERROR_SUBSCRIBE_REJECTED: return "Error (Broker rejected subscription)"; + case MQTT_CODE_ERROR_UNSUBSCRIBE_REJECTED: + return "Error (Broker rejected unsubscribe)"; #if defined(ENABLE_MQTT_CURL) case MQTT_CODE_ERROR_CURL: return "Error (libcurl)"; diff --git a/tests/test_mqtt_client.c b/tests/test_mqtt_client.c index c35525adc..e924775ad 100644 --- a/tests/test_mqtt_client.c +++ b/tests/test_mqtt_client.c @@ -452,6 +452,46 @@ TEST(subscribe_broker_rejection_returns_subscribe_rejected) subscribe.topics[0].return_code); } +#ifdef WOLFMQTT_V5 +/* A v5 UNSUBACK whose per-topic reason code has the high bit set means the + * broker refused the unsubscribe; MqttClient_Unsubscribe must surface that as + * MQTT_CODE_ERROR_UNSUBSCRIBE_REJECTED rather than success. */ +TEST(unsubscribe_broker_rejection_returns_unsubscribe_rejected) +{ + int rc; + int i; + MqttUnsubscribe unsub; + MqttTopic topics[1]; + /* v5 UNSUBACK: type=0xB0, remain=4, packet_id=43, props_len=0, + * reason=0x87 (NOT_AUTHORIZED). */ + static const byte unsuback[] = { 0xB0, 0x04, 0x00, 0x2B, 0x00, 0x87 }; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read_canned; + XMEMCPY(g_canned_buf, unsuback, sizeof(unsuback)); + g_canned_len = (int)sizeof(unsuback); + g_canned_pos = 0; + + XMEMSET(&unsub, 0, sizeof(unsub)); + XMEMSET(topics, 0, sizeof(topics)); + topics[0].topic_filter = "test/topic"; + unsub.packet_id = 43; + unsub.topic_count = 1; + unsub.topics = topics; + + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Unsubscribe(&test_client, &unsub); + } + + ASSERT_EQ(MQTT_CODE_ERROR_UNSUBSCRIBE_REJECTED, rc); +} +#endif /* WOLFMQTT_V5 */ + /* ============================================================================ * MqttClient_Disconnect Tests * ============================================================================ */ @@ -855,6 +895,9 @@ void run_mqtt_client_tests(void) RUN_TEST(subscribe_null_client); RUN_TEST(subscribe_null_subscribe); RUN_TEST(subscribe_broker_rejection_returns_subscribe_rejected); +#ifdef WOLFMQTT_V5 + RUN_TEST(unsubscribe_broker_rejection_returns_unsubscribe_rejected); +#endif /* MqttClient_Unsubscribe tests */ RUN_TEST(unsubscribe_null_client); From 7e14290e60d3ce6abd2ec2852a2384d0bcde68da Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 09:59:24 -0700 Subject: [PATCH 26/40] Address skoll review: cancel deferred retained delete on re-store and reap tombstones after delivery --- src/mqtt_broker.c | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 891b9e35d..4e77b975b 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -3212,6 +3212,9 @@ static int BrokerRetained_Store(MqttBroker* broker, const char* topic, while (cur) { if (cur->topic != NULL && XSTRCMP(cur->topic, topic) == 0) { msg = cur; + /* Re-publishing this topic cancels a deferred delete, otherwise a + * later delivery would reap the freshly stored message. */ + msg->pending_delete = 0; break; } cur = cur->next; @@ -3852,6 +3855,34 @@ static void BrokerRetained_DeliverToClient(MqttBroker* broker, if (broker->retained_delivering > 0) { broker->retained_delivering--; } + /* When the outermost delivery finishes, reap nodes a re-entrant delete + * marked after this loop had already passed them, so they stop counting + * against BROKER_MAX_RETAINED. Safe now: no delivery loop holds pointers. */ + if (broker->retained_delivering == 0) { + BrokerRetainedMsg* p = broker->retained; + BrokerRetainedMsg* pprev = NULL; + while (p != NULL) { + BrokerRetainedMsg* pnext = p->next; + if (p->pending_delete) { + if (pprev) { + pprev->next = pnext; + } + else { + broker->retained = pnext; + } + if (p->topic) WOLFMQTT_FREE(p->topic); + if (p->payload) WOLFMQTT_FREE(p->payload); + WOLFMQTT_FREE(p); + if (broker->retained_count > 0) { + broker->retained_count--; + } + } + else { + pprev = p; + } + p = pnext; + } + } #endif } #endif /* WOLFMQTT_BROKER_RETAINED */ From abb3dccc6294500f11955067671da4af067b5309 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 10:31:49 -0700 Subject: [PATCH 27/40] Add localhost SAN to broker test certs so WebSocket TLS hostname verification passes --- scripts/broker_test/ca-cert.pem | 143 ++++++-------- scripts/broker_test/server-cert.pem | 290 +++++++++++++--------------- scripts/broker_test/server-key.pem | 50 ++--- 3 files changed, 216 insertions(+), 267 deletions(-) diff --git a/scripts/broker_test/ca-cert.pem b/scripts/broker_test/ca-cert.pem index bb4abe7bc..4a7ac6a6e 100644 --- a/scripts/broker_test/ca-cert.pem +++ b/scripts/broker_test/ca-cert.pem @@ -1,93 +1,72 @@ Certificate: Data: - Version: 3 (0x2) + Version: 1 (0x0) Serial Number: - 6b:9b:70:c6:f1:a3:94:65:19:a1:08:58:ef:a7:8d:2b:7a:83:c1:da - Signature Algorithm: sha256WithRSAEncryption - Issuer: C = US, ST = Montana, L = Bozeman, O = Sawtooth, OU = Consulting, CN = www.wolfssl.com, emailAddress = info@wolfssl.com + cb:f5:0d:cd:48:75:df:5e + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=Montana, L=Bozeman, O=Sawtooth, OU=Consulting, CN=www.wolfssl.com/emailAddress=info@wolfssl.com Validity - Not Before: Dec 18 21:25:29 2024 GMT - Not After : Sep 14 21:25:29 2027 GMT - Subject: C = US, ST = Montana, L = Bozeman, O = Sawtooth, OU = Consulting, CN = www.wolfssl.com, emailAddress = info@wolfssl.com + Not Before: Jun 12 17:30:11 2026 GMT + Not After : Jun 9 17:30:11 2036 GMT + Subject: C=US, ST=Montana, L=Bozeman, O=Sawtooth, OU=Consulting, CN=www.wolfssl.com/emailAddress=info@wolfssl.com Subject Public Key Info: Public Key Algorithm: rsaEncryption - Public-Key: (2048 bit) + RSA Public-Key: (2048 bit) Modulus: - 00:bf:0c:ca:2d:14:b2:1e:84:42:5b:cd:38:1f:4a: - f2:4d:75:10:f1:b6:35:9f:df:ca:7d:03:98:d3:ac: - de:03:66:ee:2a:f1:d8:b0:7d:6e:07:54:0b:10:98: - 21:4d:80:cb:12:20:e7:cc:4f:de:45:7d:c9:72:77: - 32:ea:ca:90:bb:69:52:10:03:2f:a8:f3:95:c5:f1: - 8b:62:56:1b:ef:67:6f:a4:10:41:95:ad:0a:9b:e3: - a5:c0:b0:d2:70:76:50:30:5b:a8:e8:08:2c:7c:ed: - a7:a2:7a:8d:38:29:1c:ac:c7:ed:f2:7c:95:b0:95: - 82:7d:49:5c:38:cd:77:25:ef:bd:80:75:53:94:3c: - 3d:ca:63:5b:9f:15:b5:d3:1d:13:2f:19:d1:3c:db: - 76:3a:cc:b8:7d:c9:e5:c2:d7:da:40:6f:d8:21:dc: - 73:1b:42:2d:53:9c:fe:1a:fc:7d:ab:7a:36:3f:98: - de:84:7c:05:67:ce:6a:14:38:87:a9:f1:8c:b5:68: - cb:68:7f:71:20:2b:f5:a0:63:f5:56:2f:a3:26:d2: - b7:6f:b1:5a:17:d7:38:99:08:fe:93:58:6f:fe:c3: - 13:49:08:16:0b:a7:4d:67:00:52:31:67:23:4e:98: - ed:51:45:1d:b9:04:d9:0b:ec:d8:28:b3:4b:bd:ed: - 36:79 + 00:ce:6b:43:26:a4:36:e0:da:0b:ee:4b:84:5f:72: + df:fe:22:76:34:44:6c:c9:45:8c:8b:be:8b:f2:af: + 67:aa:74:25:48:77:e6:44:6c:80:69:9e:ae:d2:ec: + 5c:c3:e3:f2:51:4a:6c:c9:c2:c9:cb:5e:6d:74:d9: + 28:08:e9:6d:2e:6e:5b:7b:60:b8:17:cf:90:fc:dc: + 0b:fc:8f:e8:e9:4a:6f:e8:28:7d:50:d4:f3:a1:ea: + 29:62:92:15:82:f4:3b:74:88:6d:6f:ce:a5:63:bf: + 13:fc:34:03:62:ab:c7:24:6e:4e:62:69:88:6a:7f: + af:89:78:2c:64:88:0a:6e:59:b7:a1:43:43:ce:f3: + db:7a:97:af:f2:15:d1:e2:59:ea:65:8e:94:14:92: + 16:45:d6:d0:11:c1:5d:b9:04:00:96:6b:f3:cc:e6: + bb:ab:37:f7:d8:8d:29:a1:b0:d0:cc:dd:60:50:90: + 1d:f1:8d:b0:c9:01:6a:49:d2:b0:df:79:37:3c:b0: + c5:a0:4b:54:36:a0:b0:aa:c4:a1:09:66:6a:3a:07: + a6:42:d4:83:b1:9a:30:e2:8d:fd:41:01:86:2c:6a: + bf:41:5d:d6:37:20:34:9d:2b:be:de:0f:d7:d9:4e: + fe:e0:97:53:96:02:62:f3:87:55:de:47:bc:83:26: + dd:29 Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Subject Key Identifier: - 27:8E:67:11:74:C3:26:1D:3F:ED:33:63:B3:A4:D8:1D:30:E5:E8:D5 - X509v3 Authority Key Identifier: - keyid:27:8E:67:11:74:C3:26:1D:3F:ED:33:63:B3:A4:D8:1D:30:E5:E8:D5 - DirName:/C=US/ST=Montana/L=Bozeman/O=Sawtooth/OU=Consulting/CN=www.wolfssl.com/emailAddress=info@wolfssl.com - serial:6B:9B:70:C6:F1:A3:94:65:19:A1:08:58:EF:A7:8D:2B:7A:83:C1:DA - X509v3 Basic Constraints: - CA:TRUE - X509v3 Subject Alternative Name: - DNS:example.com, IP Address:127.0.0.1 - X509v3 Extended Key Usage: - TLS Web Server Authentication, TLS Web Client Authentication Signature Algorithm: sha256WithRSAEncryption - Signature Value: - 77:3b:3d:66:74:bc:97:fe:40:16:e6:ba:a5:d5:d1:84:08:89: - 69:4f:88:0d:57:a9:ef:8c:c3:97:52:c8:bd:8b:a2:49:3b:b7: - f7:5d:1e:d6:14:7f:b2:80:33:da:a0:8a:d3:e1:2f:d5:bc:33: - 9f:ea:5a:72:24:e5:f8:b8:4b:b3:df:62:90:3b:a8:21:ef:27: - 42:75:bc:60:02:8e:37:35:99:eb:a3:28:f2:65:4c:ff:7a:f8: - 8e:cc:23:6d:e5:6a:fe:22:5a:d9:b2:4f:47:c7:e0:ae:98:ef: - 94:ac:b6:4f:61:81:29:8e:e1:79:2c:46:fc:e9:1a:c3:96:1f: - 19:93:64:2e:9f:37:72:c5:e4:93:4e:61:5f:38:8e:ae:e8:39: - 19:e6:97:a8:91:d4:23:7e:1e:d2:d0:53:ec:cc:ac:a0:1d:d0: - b7:dd:b1:b7:01:2e:96:cd:85:27:e0:e7:47:e2:c1:c1:00:f6: - 94:df:77:e7:fa:c6:ef:8a:c0:7c:67:bc:ff:a0:7c:94:3b:7d: - 86:42:af:3d:83:31:ee:2a:3b:7b:f0:2c:9e:6f:e9:c4:07:81: - 24:da:05:70:4d:dd:09:ae:9e:72:b8:21:0e:8c:b2:ab:aa:4c: - 49:10:f7:76:f9:b5:0d:6c:20:d3:df:7a:06:32:8d:29:1f:28: - 1d:8d:26:33 + 57:a9:91:4b:3c:fe:1e:b3:58:58:07:82:7f:a8:33:9b:e6:2f: + f5:e6:de:d0:72:00:1a:04:6f:15:cc:01:8b:10:1f:19:e3:d2: + 8e:d9:27:48:6c:2d:ef:c2:1e:a3:97:fe:3f:bd:cf:97:5a:1b: + 85:19:20:63:e6:2a:85:6d:08:cb:b8:38:c1:38:3f:c9:88:90: + e2:48:f4:c4:22:d2:3f:78:63:f0:a8:1b:26:08:2b:23:82:2c: + f0:e9:51:21:65:58:87:ad:73:03:41:95:67:ff:e9:b9:3d:93: + 19:49:ff:39:6a:75:c6:74:3b:e9:51:d0:07:28:14:30:f9:1a: + b2:23:63:b6:25:64:f0:3a:62:bf:84:40:65:ea:38:6c:f6:86: + 13:fb:94:98:ac:03:37:6e:e5:75:28:65:20:69:b2:f9:9a:2c: + e6:af:ad:74:78:d8:5f:f4:d2:6c:4d:32:6c:3a:ab:65:6b:d7: + 3c:f9:fb:6a:18:fb:c0:88:e4:40:0b:38:cd:d4:5a:1c:b0:bb: + 18:3a:6a:18:20:0f:27:0e:ea:38:13:50:2c:f4:3a:00:dc:e9: + 1a:d5:52:21:cb:3c:7c:e1:25:e8:d5:e1:b4:2e:79:2c:45:8a: + cb:63:90:60:57:96:a0:17:6f:d8:cf:05:76:b1:51:12:86:a3: + 58:36:00:5c -----BEGIN CERTIFICATE----- -MIIE/zCCA+egAwIBAgIUa5twxvGjlGUZoQhY76eNK3qDwdowDQYJKoZIhvcNAQEL -BQAwgZQxCzAJBgNVBAYTAlVTMRAwDgYDVQQIDAdNb250YW5hMRAwDgYDVQQHDAdC -b3plbWFuMREwDwYDVQQKDAhTYXd0b290aDETMBEGA1UECwwKQ29uc3VsdGluZzEY -MBYGA1UEAwwPd3d3LndvbGZzc2wuY29tMR8wHQYJKoZIhvcNAQkBFhBpbmZvQHdv -bGZzc2wuY29tMB4XDTI0MTIxODIxMjUyOVoXDTI3MDkxNDIxMjUyOVowgZQxCzAJ -BgNVBAYTAlVTMRAwDgYDVQQIDAdNb250YW5hMRAwDgYDVQQHDAdCb3plbWFuMREw -DwYDVQQKDAhTYXd0b290aDETMBEGA1UECwwKQ29uc3VsdGluZzEYMBYGA1UEAwwP -d3d3LndvbGZzc2wuY29tMR8wHQYJKoZIhvcNAQkBFhBpbmZvQHdvbGZzc2wuY29t -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvwzKLRSyHoRCW804H0ry -TXUQ8bY1n9/KfQOY06zeA2buKvHYsH1uB1QLEJghTYDLEiDnzE/eRX3Jcncy6sqQ -u2lSEAMvqPOVxfGLYlYb72dvpBBBla0Km+OlwLDScHZQMFuo6AgsfO2nonqNOCkc -rMft8nyVsJWCfUlcOM13Je+9gHVTlDw9ymNbnxW10x0TLxnRPNt2Osy4fcnlwtfa -QG/YIdxzG0ItU5z+Gvx9q3o2P5jehHwFZ85qFDiHqfGMtWjLaH9xICv1oGP1Vi+j -JtK3b7FaF9c4mQj+k1hv/sMTSQgWC6dNZwBSMWcjTpjtUUUduQTZC+zYKLNLve02 -eQIDAQABo4IBRTCCAUEwHQYDVR0OBBYEFCeOZxF0wyYdP+0zY7Ok2B0w5ejVMIHU -BgNVHSMEgcwwgcmAFCeOZxF0wyYdP+0zY7Ok2B0w5ejVoYGapIGXMIGUMQswCQYD -VQQGEwJVUzEQMA4GA1UECAwHTW9udGFuYTEQMA4GA1UEBwwHQm96ZW1hbjERMA8G -A1UECgwIU2F3dG9vdGgxEzARBgNVBAsMCkNvbnN1bHRpbmcxGDAWBgNVBAMMD3d3 -dy53b2xmc3NsLmNvbTEfMB0GCSqGSIb3DQEJARYQaW5mb0B3b2xmc3NsLmNvbYIU -a5twxvGjlGUZoQhY76eNK3qDwdowDAYDVR0TBAUwAwEB/zAcBgNVHREEFTATggtl -eGFtcGxlLmNvbYcEfwAAATAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw -DQYJKoZIhvcNAQELBQADggEBAHc7PWZ0vJf+QBbmuqXV0YQIiWlPiA1Xqe+Mw5dS -yL2Lokk7t/ddHtYUf7KAM9qgitPhL9W8M5/qWnIk5fi4S7PfYpA7qCHvJ0J1vGAC -jjc1meujKPJlTP96+I7MI23lav4iWtmyT0fH4K6Y75Sstk9hgSmO4XksRvzpGsOW -HxmTZC6fN3LF5JNOYV84jq7oORnml6iR1CN+HtLQU+zMrKAd0LfdsbcBLpbNhSfg -50fiwcEA9pTfd+f6xu+KwHxnvP+gfJQ7fYZCrz2DMe4qO3vwLJ5v6cQHgSTaBXBN -3QmunnK4IQ6MsquqTEkQ93b5tQ1sINPfegYyjSkfKB2NJjM= +MIIDpjCCAo4CCQDL9Q3NSHXfXjANBgkqhkiG9w0BAQsFADCBlDELMAkGA1UEBhMC +VVMxEDAOBgNVBAgMB01vbnRhbmExEDAOBgNVBAcMB0JvemVtYW4xETAPBgNVBAoM +CFNhd3Rvb3RoMRMwEQYDVQQLDApDb25zdWx0aW5nMRgwFgYDVQQDDA93d3cud29s +ZnNzbC5jb20xHzAdBgkqhkiG9w0BCQEWEGluZm9Ad29sZnNzbC5jb20wHhcNMjYw +NjEyMTczMDExWhcNMzYwNjA5MTczMDExWjCBlDELMAkGA1UEBhMCVVMxEDAOBgNV +BAgMB01vbnRhbmExEDAOBgNVBAcMB0JvemVtYW4xETAPBgNVBAoMCFNhd3Rvb3Ro +MRMwEQYDVQQLDApDb25zdWx0aW5nMRgwFgYDVQQDDA93d3cud29sZnNzbC5jb20x +HzAdBgkqhkiG9w0BCQEWEGluZm9Ad29sZnNzbC5jb20wggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDOa0MmpDbg2gvuS4Rfct/+InY0RGzJRYyLvovyr2eq +dCVId+ZEbIBpnq7S7FzD4/JRSmzJwsnLXm102SgI6W0ublt7YLgXz5D83Av8j+jp +Sm/oKH1Q1POh6ilikhWC9Dt0iG1vzqVjvxP8NANiq8ckbk5iaYhqf6+JeCxkiApu +WbehQ0PO89t6l6/yFdHiWepljpQUkhZF1tARwV25BACWa/PM5rurN/fYjSmhsNDM +3WBQkB3xjbDJAWpJ0rDfeTc8sMWgS1Q2oLCqxKEJZmo6B6ZC1IOxmjDijf1BAYYs +ar9BXdY3IDSdK77eD9fZTv7gl1OWAmLzh1XeR7yDJt0pAgMBAAEwDQYJKoZIhvcN +AQELBQADggEBAFepkUs8/h6zWFgHgn+oM5vmL/Xm3tByABoEbxXMAYsQHxnj0o7Z +J0hsLe/CHqOX/j+9z5daG4UZIGPmKoVtCMu4OME4P8mIkOJI9MQi0j94Y/CoGyYI +KyOCLPDpUSFlWIetcwNBlWf/6bk9kxlJ/zlqdcZ0O+lR0AcoFDD5GrIjY7YlZPA6 +Yr+EQGXqOGz2hhP7lJisAzdu5XUoZSBpsvmaLOavrXR42F/00mxNMmw6q2Vr1zz5 ++2oY+8CI5EALOM3UWhywuxg6ahggDycO6jgTUCz0OgDc6RrVUiHLPHzhJejV4bQu +eSxFistjkGBXlqAXb9jPBXaxURKGo1g2AFw= -----END CERTIFICATE----- diff --git a/scripts/broker_test/server-cert.pem b/scripts/broker_test/server-cert.pem index 6fc3db772..ecb280af4 100644 --- a/scripts/broker_test/server-cert.pem +++ b/scripts/broker_test/server-cert.pem @@ -1,185 +1,155 @@ Certificate: Data: Version: 3 (0x2) - Serial Number: 1 (0x1) - Signature Algorithm: sha256WithRSAEncryption - Issuer: C = US, ST = Montana, L = Bozeman, O = Sawtooth, OU = Consulting, CN = www.wolfssl.com, emailAddress = info@wolfssl.com + Serial Number: + c5:e2:b6:41:60:d5:38:e9 + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=Montana, L=Bozeman, O=Sawtooth, OU=Consulting, CN=www.wolfssl.com/emailAddress=info@wolfssl.com Validity - Not Before: Dec 18 21:25:30 2024 GMT - Not After : Sep 14 21:25:30 2027 GMT - Subject: C = US, ST = Montana, L = Bozeman, O = wolfSSL, OU = Support, CN = www.wolfssl.com, emailAddress = info@wolfssl.com + Not Before: Jun 12 17:30:11 2026 GMT + Not After : Jun 9 17:30:11 2036 GMT + Subject: C=US, ST=Montana, L=Bozeman, O=wolfSSL, OU=Support, CN=www.wolfssl.com/emailAddress=info@wolfssl.com Subject Public Key Info: Public Key Algorithm: rsaEncryption - Public-Key: (2048 bit) + RSA Public-Key: (2048 bit) Modulus: - 00:c0:95:08:e1:57:41:f2:71:6d:b7:d2:45:41:27: - 01:65:c6:45:ae:f2:bc:24:30:b8:95:ce:2f:4e:d6: - f6:1c:88:bc:7c:9f:fb:a8:67:7f:fe:5c:9c:51:75: - f7:8a:ca:07:e7:35:2f:8f:e1:bd:7b:c0:2f:7c:ab: - 64:a8:17:fc:ca:5d:7b:ba:e0:21:e5:72:2e:6f:2e: - 86:d8:95:73:da:ac:1b:53:b9:5f:3f:d7:19:0d:25: - 4f:e1:63:63:51:8b:0b:64:3f:ad:43:b8:a5:1c:5c: - 34:b3:ae:00:a0:63:c5:f6:7f:0b:59:68:78:73:a6: - 8c:18:a9:02:6d:af:c3:19:01:2e:b8:10:e3:c6:cc: - 40:b4:69:a3:46:33:69:87:6e:c4:bb:17:a6:f3:e8: - dd:ad:73:bc:7b:2f:21:b5:fd:66:51:0c:bd:54:b3: - e1:6d:5f:1c:bc:23:73:d1:09:03:89:14:d2:10:b9: - 64:c3:2a:d0:a1:96:4a:bc:e1:d4:1a:5b:c7:a0:c0: - c1:63:78:0f:44:37:30:32:96:80:32:23:95:a1:77: - ba:13:d2:97:73:e2:5d:25:c9:6a:0d:c3:39:60:a4: - b4:b0:69:42:42:09:e9:d8:08:bc:33:20:b3:58:22: - a7:aa:eb:c4:e1:e6:61:83:c5:d2:96:df:d9:d0:4f: - ad:d7 + 00:ca:2c:8c:fc:f7:ae:fb:c1:4b:e9:c6:1a:38:52: + e7:07:f8:ec:9b:63:6d:87:9e:32:b4:bd:22:cb:9c: + ae:a7:64:65:d3:42:c5:6a:49:58:7b:be:5c:a8:d7: + 0a:3d:93:a8:27:fd:aa:eb:16:46:8e:61:08:ca:45: + eb:0b:e7:54:bc:d9:20:69:6b:33:c6:e6:73:f9:40: + cc:76:34:3b:7a:c6:5f:26:29:56:a8:82:4d:2d:78: + 2d:53:07:f0:4e:f5:01:b6:48:54:e4:88:af:2d:75: + 39:bd:b6:71:60:2b:52:3b:44:d5:dc:8d:77:56:0b: + 9e:b2:7a:a3:c8:dd:48:50:64:93:5d:ce:c8:d0:08: + e4:4a:6b:f8:2b:02:6b:c4:af:11:44:c2:9b:a3:40: + 2b:2b:a2:f3:74:05:08:7c:0d:74:90:4c:5e:33:f7: + 1b:32:1f:08:03:aa:9c:93:67:8b:86:1c:54:f1:8a: + e9:a7:f7:67:7d:6e:4b:8e:23:01:7d:0e:ba:53:54: + 00:ce:3b:34:1d:5c:f4:10:bd:8f:c0:3f:85:1c:18: + 68:80:ee:78:8b:2f:50:47:f6:8a:8d:9d:24:50:ef: + c5:fc:f1:f7:1e:be:28:ce:98:8f:61:06:5c:c1:e9: + 94:f4:0c:f2:10:71:9e:23:6c:5b:8a:7d:cc:03:ba: + b9:2f Exponent: 65537 (0x10001) X509v3 extensions: - X509v3 Subject Key Identifier: - B3:11:32:C9:92:98:84:E2:C9:F8:D0:3B:6E:03:42:CA:1F:0E:8E:3C - X509v3 Authority Key Identifier: - keyid:27:8E:67:11:74:C3:26:1D:3F:ED:33:63:B3:A4:D8:1D:30:E5:E8:D5 - DirName:/C=US/ST=Montana/L=Bozeman/O=Sawtooth/OU=Consulting/CN=www.wolfssl.com/emailAddress=info@wolfssl.com - serial:6B:9B:70:C6:F1:A3:94:65:19:A1:08:58:EF:A7:8D:2B:7A:83:C1:DA - X509v3 Basic Constraints: - CA:TRUE X509v3 Subject Alternative Name: - DNS:example.com, IP Address:127.0.0.1 + DNS:localhost, DNS:example.com, IP Address:127.0.0.1 + X509v3 Basic Constraints: + CA:FALSE + X509v3 Key Usage: + Digital Signature, Key Encipherment X509v3 Extended Key Usage: - TLS Web Server Authentication, TLS Web Client Authentication + TLS Web Server Authentication Signature Algorithm: sha256WithRSAEncryption - Signature Value: - 8a:f1:4e:e8:9f:59:b2:d9:13:ac:fc:42:c4:81:34:9f:6b:39: - 57:9c:e9:92:5d:41:ac:05:35:b1:26:93:4d:4a:da:f8:51:82: - d2:8d:7f:d3:5c:6e:29:80:8d:9b:02:10:2b:64:f5:d1:31:06: - fa:85:2b:8f:63:32:14:76:7a:39:15:f3:4e:dd:fd:e2:2c:90: - 15:d1:6f:73:87:ee:e6:c8:eb:ad:40:d5:e8:94:1f:a6:7e:26: - 5b:87:ba:0f:06:5a:4d:55:7a:aa:c4:09:34:8b:f7:e5:cc:d6: - b7:6c:46:6d:a1:e6:66:66:4c:4b:e5:12:31:37:54:49:64:a5: - 66:eb:e0:c6:a1:49:f8:4d:c3:d3:55:a4:05:d2:ac:fb:e1:c8: - 69:30:4b:98:fd:72:1a:ab:9f:86:eb:0d:bd:7c:a6:3d:81:d9: - 01:a7:8a:79:ab:3c:ce:e5:b6:c3:1b:ef:7d:5e:37:7b:37:7c: - 91:89:59:11:21:11:7c:05:80:e1:a8:d6:f9:35:da:1b:86:06: - 5a:32:67:6c:a9:2b:e0:31:7b:89:53:37:42:af:34:a4:53:d2: - 7c:91:50:63:3a:8e:4a:1f:a3:90:4e:7c:41:59:1d:eb:7b:a2: - 14:87:ba:76:36:a4:77:46:34:f2:55:50:f0:24:9f:83:83:da: - a6:aa:3c:c8 + b0:14:09:45:a7:16:c2:fd:01:fa:a5:ca:dd:14:3b:5d:0d:80: + a2:d2:d6:ef:7c:bd:c1:49:98:05:35:52:6e:67:c7:9f:23:f2: + ad:b1:4c:0e:50:b2:c7:b0:0e:b6:44:42:5b:fe:21:29:71:6c: + 54:36:a9:a3:31:c6:1f:04:ee:a3:50:b5:fd:33:f9:d2:c4:58: + 11:e1:4d:c3:c3:b3:1f:db:42:95:11:ac:e6:f8:33:d5:46:97: + 2c:47:db:97:53:f1:7b:3f:14:bb:8a:b1:2f:f6:0a:11:82:8c: + ce:45:3a:25:91:a3:73:3a:95:90:89:d2:b2:cc:91:8c:d8:2a: + ff:3b:3d:40:ca:6d:d9:5d:05:7e:52:88:8b:b9:2b:a2:52:be: + 42:0a:09:a8:ba:2b:e3:5c:a6:f3:89:4a:50:40:e3:1a:a6:24: + 2f:b6:de:ce:7d:72:6c:fa:d3:2f:52:0f:16:00:9c:95:bf:54: + 20:1e:ab:99:25:66:61:f9:d5:1e:8b:0d:35:57:8b:d4:5e:16: + c8:3d:f3:7d:a4:2f:2e:55:1d:3f:e8:01:e7:eb:17:37:0c:0d: + ba:1e:9e:a7:33:9e:a9:90:12:36:4c:62:63:e5:02:f2:aa:33: + a6:99:bf:89:02:98:7d:cf:ff:cd:0f:f9:54:9a:9d:e7:83:dd: + 0b:8c:11:39 -----BEGIN CERTIFICATE----- -MIIE6DCCA9CgAwIBAgIBATANBgkqhkiG9w0BAQsFADCBlDELMAkGA1UEBhMCVVMx -EDAOBgNVBAgMB01vbnRhbmExEDAOBgNVBAcMB0JvemVtYW4xETAPBgNVBAoMCFNh -d3Rvb3RoMRMwEQYDVQQLDApDb25zdWx0aW5nMRgwFgYDVQQDDA93d3cud29sZnNz -bC5jb20xHzAdBgkqhkiG9w0BCQEWEGluZm9Ad29sZnNzbC5jb20wHhcNMjQxMjE4 -MjEyNTMwWhcNMjcwOTE0MjEyNTMwWjCBkDELMAkGA1UEBhMCVVMxEDAOBgNVBAgM -B01vbnRhbmExEDAOBgNVBAcMB0JvemVtYW4xEDAOBgNVBAoMB3dvbGZTU0wxEDAO -BgNVBAsMB1N1cHBvcnQxGDAWBgNVBAMMD3d3dy53b2xmc3NsLmNvbTEfMB0GCSqG -SIb3DQEJARYQaW5mb0B3b2xmc3NsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEP -ADCCAQoCggEBAMCVCOFXQfJxbbfSRUEnAWXGRa7yvCQwuJXOL07W9hyIvHyf+6hn -f/5cnFF194rKB+c1L4/hvXvAL3yrZKgX/Mpde7rgIeVyLm8uhtiVc9qsG1O5Xz/X -GQ0lT+FjY1GLC2Q/rUO4pRxcNLOuAKBjxfZ/C1loeHOmjBipAm2vwxkBLrgQ48bM -QLRpo0YzaYduxLsXpvPo3a1zvHsvIbX9ZlEMvVSz4W1fHLwjc9EJA4kU0hC5ZMMq -0KGWSrzh1Bpbx6DAwWN4D0Q3MDKWgDIjlaF3uhPSl3PiXSXJag3DOWCktLBpQkIJ -6dgIvDMgs1gip6rrxOHmYYPF0pbf2dBPrdcCAwEAAaOCAUUwggFBMB0GA1UdDgQW -BBSzETLJkpiE4sn40DtuA0LKHw6OPDCB1AYDVR0jBIHMMIHJgBQnjmcRdMMmHT/t -M2OzpNgdMOXo1aGBmqSBlzCBlDELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB01vbnRh -bmExEDAOBgNVBAcMB0JvemVtYW4xETAPBgNVBAoMCFNhd3Rvb3RoMRMwEQYDVQQL -DApDb25zdWx0aW5nMRgwFgYDVQQDDA93d3cud29sZnNzbC5jb20xHzAdBgkqhkiG -9w0BCQEWEGluZm9Ad29sZnNzbC5jb22CFGubcMbxo5RlGaEIWO+njSt6g8HaMAwG -A1UdEwQFMAMBAf8wHAYDVR0RBBUwE4ILZXhhbXBsZS5jb22HBH8AAAEwHQYDVR0l -BBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMA0GCSqGSIb3DQEBCwUAA4IBAQCK8U7o -n1my2ROs/ELEgTSfazlXnOmSXUGsBTWxJpNNStr4UYLSjX/TXG4pgI2bAhArZPXR -MQb6hSuPYzIUdno5FfNO3f3iLJAV0W9zh+7myOutQNXolB+mfiZbh7oPBlpNVXqq -xAk0i/flzNa3bEZtoeZmZkxL5RIxN1RJZKVm6+DGoUn4TcPTVaQF0qz74chpMEuY -/XIaq5+G6w29fKY9gdkBp4p5qzzO5bbDG+99Xjd7N3yRiVkRIRF8BYDhqNb5Ndob -hgZaMmdsqSvgMXuJUzdCrzSkU9J8kVBjOo5KH6OQTnxBWR3re6IUh7p2NqR3RjTy -VVDwJJ+Dg9qmqjzI +MIIEATCCAumgAwIBAgIJAMXitkFg1TjpMA0GCSqGSIb3DQEBCwUAMIGUMQswCQYD +VQQGEwJVUzEQMA4GA1UECAwHTW9udGFuYTEQMA4GA1UEBwwHQm96ZW1hbjERMA8G +A1UECgwIU2F3dG9vdGgxEzARBgNVBAsMCkNvbnN1bHRpbmcxGDAWBgNVBAMMD3d3 +dy53b2xmc3NsLmNvbTEfMB0GCSqGSIb3DQEJARYQaW5mb0B3b2xmc3NsLmNvbTAe +Fw0yNjA2MTIxNzMwMTFaFw0zNjA2MDkxNzMwMTFaMIGQMQswCQYDVQQGEwJVUzEQ +MA4GA1UECAwHTW9udGFuYTEQMA4GA1UEBwwHQm96ZW1hbjEQMA4GA1UECgwHd29s +ZlNTTDEQMA4GA1UECwwHU3VwcG9ydDEYMBYGA1UEAwwPd3d3LndvbGZzc2wuY29t +MR8wHQYJKoZIhvcNAQkBFhBpbmZvQHdvbGZzc2wuY29tMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAyiyM/Peu+8FL6cYaOFLnB/jsm2Nth54ytL0iy5yu +p2Rl00LFaklYe75cqNcKPZOoJ/2q6xZGjmEIykXrC+dUvNkgaWszxuZz+UDMdjQ7 +esZfJilWqIJNLXgtUwfwTvUBtkhU5IivLXU5vbZxYCtSO0TV3I13VguesnqjyN1I +UGSTXc7I0AjkSmv4KwJrxK8RRMKbo0ArK6LzdAUIfA10kExeM/cbMh8IA6qck2eL +hhxU8Yrpp/dnfW5LjiMBfQ66U1QAzjs0HVz0EL2PwD+FHBhogO54iy9QR/aKjZ0k +UO/F/PH3Hr4ozpiPYQZcwemU9AzyEHGeI2xbin3MA7q5LwIDAQABo1gwVjAnBgNV +HREEIDAegglsb2NhbGhvc3SCC2V4YW1wbGUuY29thwR/AAABMAkGA1UdEwQCMAAw +CwYDVR0PBAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA0GCSqGSIb3DQEBCwUA +A4IBAQCwFAlFpxbC/QH6pcrdFDtdDYCi0tbvfL3BSZgFNVJuZ8efI/KtsUwOULLH +sA62REJb/iEpcWxUNqmjMcYfBO6jULX9M/nSxFgR4U3Dw7Mf20KVEazm+DPVRpcs +R9uXU/F7PxS7irEv9goRgozORTolkaNzOpWQidKyzJGM2Cr/Oz1Aym3ZXQV+UoiL +uSuiUr5CCgmouivjXKbziUpQQOMapiQvtt7OfXJs+tMvUg8WAJyVv1QgHquZJWZh ++dUeiw01V4vUXhbIPfN9pC8uVR0/6AHn6xc3DA26Hp6nM56pkBI2TGJj5QLyqjOm +mb+JAph9z//ND/lUmp3ng90LjBE5 -----END CERTIFICATE----- Certificate: Data: - Version: 3 (0x2) + Version: 1 (0x0) Serial Number: - 6b:9b:70:c6:f1:a3:94:65:19:a1:08:58:ef:a7:8d:2b:7a:83:c1:da - Signature Algorithm: sha256WithRSAEncryption - Issuer: C = US, ST = Montana, L = Bozeman, O = Sawtooth, OU = Consulting, CN = www.wolfssl.com, emailAddress = info@wolfssl.com + cb:f5:0d:cd:48:75:df:5e + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=Montana, L=Bozeman, O=Sawtooth, OU=Consulting, CN=www.wolfssl.com/emailAddress=info@wolfssl.com Validity - Not Before: Dec 18 21:25:29 2024 GMT - Not After : Sep 14 21:25:29 2027 GMT - Subject: C = US, ST = Montana, L = Bozeman, O = Sawtooth, OU = Consulting, CN = www.wolfssl.com, emailAddress = info@wolfssl.com + Not Before: Jun 12 17:30:11 2026 GMT + Not After : Jun 9 17:30:11 2036 GMT + Subject: C=US, ST=Montana, L=Bozeman, O=Sawtooth, OU=Consulting, CN=www.wolfssl.com/emailAddress=info@wolfssl.com Subject Public Key Info: Public Key Algorithm: rsaEncryption - Public-Key: (2048 bit) + RSA Public-Key: (2048 bit) Modulus: - 00:bf:0c:ca:2d:14:b2:1e:84:42:5b:cd:38:1f:4a: - f2:4d:75:10:f1:b6:35:9f:df:ca:7d:03:98:d3:ac: - de:03:66:ee:2a:f1:d8:b0:7d:6e:07:54:0b:10:98: - 21:4d:80:cb:12:20:e7:cc:4f:de:45:7d:c9:72:77: - 32:ea:ca:90:bb:69:52:10:03:2f:a8:f3:95:c5:f1: - 8b:62:56:1b:ef:67:6f:a4:10:41:95:ad:0a:9b:e3: - a5:c0:b0:d2:70:76:50:30:5b:a8:e8:08:2c:7c:ed: - a7:a2:7a:8d:38:29:1c:ac:c7:ed:f2:7c:95:b0:95: - 82:7d:49:5c:38:cd:77:25:ef:bd:80:75:53:94:3c: - 3d:ca:63:5b:9f:15:b5:d3:1d:13:2f:19:d1:3c:db: - 76:3a:cc:b8:7d:c9:e5:c2:d7:da:40:6f:d8:21:dc: - 73:1b:42:2d:53:9c:fe:1a:fc:7d:ab:7a:36:3f:98: - de:84:7c:05:67:ce:6a:14:38:87:a9:f1:8c:b5:68: - cb:68:7f:71:20:2b:f5:a0:63:f5:56:2f:a3:26:d2: - b7:6f:b1:5a:17:d7:38:99:08:fe:93:58:6f:fe:c3: - 13:49:08:16:0b:a7:4d:67:00:52:31:67:23:4e:98: - ed:51:45:1d:b9:04:d9:0b:ec:d8:28:b3:4b:bd:ed: - 36:79 + 00:ce:6b:43:26:a4:36:e0:da:0b:ee:4b:84:5f:72: + df:fe:22:76:34:44:6c:c9:45:8c:8b:be:8b:f2:af: + 67:aa:74:25:48:77:e6:44:6c:80:69:9e:ae:d2:ec: + 5c:c3:e3:f2:51:4a:6c:c9:c2:c9:cb:5e:6d:74:d9: + 28:08:e9:6d:2e:6e:5b:7b:60:b8:17:cf:90:fc:dc: + 0b:fc:8f:e8:e9:4a:6f:e8:28:7d:50:d4:f3:a1:ea: + 29:62:92:15:82:f4:3b:74:88:6d:6f:ce:a5:63:bf: + 13:fc:34:03:62:ab:c7:24:6e:4e:62:69:88:6a:7f: + af:89:78:2c:64:88:0a:6e:59:b7:a1:43:43:ce:f3: + db:7a:97:af:f2:15:d1:e2:59:ea:65:8e:94:14:92: + 16:45:d6:d0:11:c1:5d:b9:04:00:96:6b:f3:cc:e6: + bb:ab:37:f7:d8:8d:29:a1:b0:d0:cc:dd:60:50:90: + 1d:f1:8d:b0:c9:01:6a:49:d2:b0:df:79:37:3c:b0: + c5:a0:4b:54:36:a0:b0:aa:c4:a1:09:66:6a:3a:07: + a6:42:d4:83:b1:9a:30:e2:8d:fd:41:01:86:2c:6a: + bf:41:5d:d6:37:20:34:9d:2b:be:de:0f:d7:d9:4e: + fe:e0:97:53:96:02:62:f3:87:55:de:47:bc:83:26: + dd:29 Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Subject Key Identifier: - 27:8E:67:11:74:C3:26:1D:3F:ED:33:63:B3:A4:D8:1D:30:E5:E8:D5 - X509v3 Authority Key Identifier: - keyid:27:8E:67:11:74:C3:26:1D:3F:ED:33:63:B3:A4:D8:1D:30:E5:E8:D5 - DirName:/C=US/ST=Montana/L=Bozeman/O=Sawtooth/OU=Consulting/CN=www.wolfssl.com/emailAddress=info@wolfssl.com - serial:6B:9B:70:C6:F1:A3:94:65:19:A1:08:58:EF:A7:8D:2B:7A:83:C1:DA - X509v3 Basic Constraints: - CA:TRUE - X509v3 Subject Alternative Name: - DNS:example.com, IP Address:127.0.0.1 - X509v3 Extended Key Usage: - TLS Web Server Authentication, TLS Web Client Authentication Signature Algorithm: sha256WithRSAEncryption - Signature Value: - 77:3b:3d:66:74:bc:97:fe:40:16:e6:ba:a5:d5:d1:84:08:89: - 69:4f:88:0d:57:a9:ef:8c:c3:97:52:c8:bd:8b:a2:49:3b:b7: - f7:5d:1e:d6:14:7f:b2:80:33:da:a0:8a:d3:e1:2f:d5:bc:33: - 9f:ea:5a:72:24:e5:f8:b8:4b:b3:df:62:90:3b:a8:21:ef:27: - 42:75:bc:60:02:8e:37:35:99:eb:a3:28:f2:65:4c:ff:7a:f8: - 8e:cc:23:6d:e5:6a:fe:22:5a:d9:b2:4f:47:c7:e0:ae:98:ef: - 94:ac:b6:4f:61:81:29:8e:e1:79:2c:46:fc:e9:1a:c3:96:1f: - 19:93:64:2e:9f:37:72:c5:e4:93:4e:61:5f:38:8e:ae:e8:39: - 19:e6:97:a8:91:d4:23:7e:1e:d2:d0:53:ec:cc:ac:a0:1d:d0: - b7:dd:b1:b7:01:2e:96:cd:85:27:e0:e7:47:e2:c1:c1:00:f6: - 94:df:77:e7:fa:c6:ef:8a:c0:7c:67:bc:ff:a0:7c:94:3b:7d: - 86:42:af:3d:83:31:ee:2a:3b:7b:f0:2c:9e:6f:e9:c4:07:81: - 24:da:05:70:4d:dd:09:ae:9e:72:b8:21:0e:8c:b2:ab:aa:4c: - 49:10:f7:76:f9:b5:0d:6c:20:d3:df:7a:06:32:8d:29:1f:28: - 1d:8d:26:33 + 57:a9:91:4b:3c:fe:1e:b3:58:58:07:82:7f:a8:33:9b:e6:2f: + f5:e6:de:d0:72:00:1a:04:6f:15:cc:01:8b:10:1f:19:e3:d2: + 8e:d9:27:48:6c:2d:ef:c2:1e:a3:97:fe:3f:bd:cf:97:5a:1b: + 85:19:20:63:e6:2a:85:6d:08:cb:b8:38:c1:38:3f:c9:88:90: + e2:48:f4:c4:22:d2:3f:78:63:f0:a8:1b:26:08:2b:23:82:2c: + f0:e9:51:21:65:58:87:ad:73:03:41:95:67:ff:e9:b9:3d:93: + 19:49:ff:39:6a:75:c6:74:3b:e9:51:d0:07:28:14:30:f9:1a: + b2:23:63:b6:25:64:f0:3a:62:bf:84:40:65:ea:38:6c:f6:86: + 13:fb:94:98:ac:03:37:6e:e5:75:28:65:20:69:b2:f9:9a:2c: + e6:af:ad:74:78:d8:5f:f4:d2:6c:4d:32:6c:3a:ab:65:6b:d7: + 3c:f9:fb:6a:18:fb:c0:88:e4:40:0b:38:cd:d4:5a:1c:b0:bb: + 18:3a:6a:18:20:0f:27:0e:ea:38:13:50:2c:f4:3a:00:dc:e9: + 1a:d5:52:21:cb:3c:7c:e1:25:e8:d5:e1:b4:2e:79:2c:45:8a: + cb:63:90:60:57:96:a0:17:6f:d8:cf:05:76:b1:51:12:86:a3: + 58:36:00:5c -----BEGIN CERTIFICATE----- -MIIE/zCCA+egAwIBAgIUa5twxvGjlGUZoQhY76eNK3qDwdowDQYJKoZIhvcNAQEL -BQAwgZQxCzAJBgNVBAYTAlVTMRAwDgYDVQQIDAdNb250YW5hMRAwDgYDVQQHDAdC -b3plbWFuMREwDwYDVQQKDAhTYXd0b290aDETMBEGA1UECwwKQ29uc3VsdGluZzEY -MBYGA1UEAwwPd3d3LndvbGZzc2wuY29tMR8wHQYJKoZIhvcNAQkBFhBpbmZvQHdv -bGZzc2wuY29tMB4XDTI0MTIxODIxMjUyOVoXDTI3MDkxNDIxMjUyOVowgZQxCzAJ -BgNVBAYTAlVTMRAwDgYDVQQIDAdNb250YW5hMRAwDgYDVQQHDAdCb3plbWFuMREw -DwYDVQQKDAhTYXd0b290aDETMBEGA1UECwwKQ29uc3VsdGluZzEYMBYGA1UEAwwP -d3d3LndvbGZzc2wuY29tMR8wHQYJKoZIhvcNAQkBFhBpbmZvQHdvbGZzc2wuY29t -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvwzKLRSyHoRCW804H0ry -TXUQ8bY1n9/KfQOY06zeA2buKvHYsH1uB1QLEJghTYDLEiDnzE/eRX3Jcncy6sqQ -u2lSEAMvqPOVxfGLYlYb72dvpBBBla0Km+OlwLDScHZQMFuo6AgsfO2nonqNOCkc -rMft8nyVsJWCfUlcOM13Je+9gHVTlDw9ymNbnxW10x0TLxnRPNt2Osy4fcnlwtfa -QG/YIdxzG0ItU5z+Gvx9q3o2P5jehHwFZ85qFDiHqfGMtWjLaH9xICv1oGP1Vi+j -JtK3b7FaF9c4mQj+k1hv/sMTSQgWC6dNZwBSMWcjTpjtUUUduQTZC+zYKLNLve02 -eQIDAQABo4IBRTCCAUEwHQYDVR0OBBYEFCeOZxF0wyYdP+0zY7Ok2B0w5ejVMIHU -BgNVHSMEgcwwgcmAFCeOZxF0wyYdP+0zY7Ok2B0w5ejVoYGapIGXMIGUMQswCQYD -VQQGEwJVUzEQMA4GA1UECAwHTW9udGFuYTEQMA4GA1UEBwwHQm96ZW1hbjERMA8G -A1UECgwIU2F3dG9vdGgxEzARBgNVBAsMCkNvbnN1bHRpbmcxGDAWBgNVBAMMD3d3 -dy53b2xmc3NsLmNvbTEfMB0GCSqGSIb3DQEJARYQaW5mb0B3b2xmc3NsLmNvbYIU -a5twxvGjlGUZoQhY76eNK3qDwdowDAYDVR0TBAUwAwEB/zAcBgNVHREEFTATggtl -eGFtcGxlLmNvbYcEfwAAATAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw -DQYJKoZIhvcNAQELBQADggEBAHc7PWZ0vJf+QBbmuqXV0YQIiWlPiA1Xqe+Mw5dS -yL2Lokk7t/ddHtYUf7KAM9qgitPhL9W8M5/qWnIk5fi4S7PfYpA7qCHvJ0J1vGAC -jjc1meujKPJlTP96+I7MI23lav4iWtmyT0fH4K6Y75Sstk9hgSmO4XksRvzpGsOW -HxmTZC6fN3LF5JNOYV84jq7oORnml6iR1CN+HtLQU+zMrKAd0LfdsbcBLpbNhSfg -50fiwcEA9pTfd+f6xu+KwHxnvP+gfJQ7fYZCrz2DMe4qO3vwLJ5v6cQHgSTaBXBN -3QmunnK4IQ6MsquqTEkQ93b5tQ1sINPfegYyjSkfKB2NJjM= +MIIDpjCCAo4CCQDL9Q3NSHXfXjANBgkqhkiG9w0BAQsFADCBlDELMAkGA1UEBhMC +VVMxEDAOBgNVBAgMB01vbnRhbmExEDAOBgNVBAcMB0JvemVtYW4xETAPBgNVBAoM +CFNhd3Rvb3RoMRMwEQYDVQQLDApDb25zdWx0aW5nMRgwFgYDVQQDDA93d3cud29s +ZnNzbC5jb20xHzAdBgkqhkiG9w0BCQEWEGluZm9Ad29sZnNzbC5jb20wHhcNMjYw +NjEyMTczMDExWhcNMzYwNjA5MTczMDExWjCBlDELMAkGA1UEBhMCVVMxEDAOBgNV +BAgMB01vbnRhbmExEDAOBgNVBAcMB0JvemVtYW4xETAPBgNVBAoMCFNhd3Rvb3Ro +MRMwEQYDVQQLDApDb25zdWx0aW5nMRgwFgYDVQQDDA93d3cud29sZnNzbC5jb20x +HzAdBgkqhkiG9w0BCQEWEGluZm9Ad29sZnNzbC5jb20wggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDOa0MmpDbg2gvuS4Rfct/+InY0RGzJRYyLvovyr2eq +dCVId+ZEbIBpnq7S7FzD4/JRSmzJwsnLXm102SgI6W0ublt7YLgXz5D83Av8j+jp +Sm/oKH1Q1POh6ilikhWC9Dt0iG1vzqVjvxP8NANiq8ckbk5iaYhqf6+JeCxkiApu +WbehQ0PO89t6l6/yFdHiWepljpQUkhZF1tARwV25BACWa/PM5rurN/fYjSmhsNDM +3WBQkB3xjbDJAWpJ0rDfeTc8sMWgS1Q2oLCqxKEJZmo6B6ZC1IOxmjDijf1BAYYs +ar9BXdY3IDSdK77eD9fZTv7gl1OWAmLzh1XeR7yDJt0pAgMBAAEwDQYJKoZIhvcN +AQELBQADggEBAFepkUs8/h6zWFgHgn+oM5vmL/Xm3tByABoEbxXMAYsQHxnj0o7Z +J0hsLe/CHqOX/j+9z5daG4UZIGPmKoVtCMu4OME4P8mIkOJI9MQi0j94Y/CoGyYI +KyOCLPDpUSFlWIetcwNBlWf/6bk9kxlJ/zlqdcZ0O+lR0AcoFDD5GrIjY7YlZPA6 +Yr+EQGXqOGz2hhP7lJisAzdu5XUoZSBpsvmaLOavrXR42F/00mxNMmw6q2Vr1zz5 ++2oY+8CI5EALOM3UWhywuxg6ahggDycO6jgTUCz0OgDc6RrVUiHLPHzhJejV4bQu +eSxFistjkGBXlqAXb9jPBXaxURKGo1g2AFw= -----END CERTIFICATE----- diff --git a/scripts/broker_test/server-key.pem b/scripts/broker_test/server-key.pem index d1627f4d4..ede188463 100644 --- a/scripts/broker_test/server-key.pem +++ b/scripts/broker_test/server-key.pem @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEAwJUI4VdB8nFtt9JFQScBZcZFrvK8JDC4lc4vTtb2HIi8fJ/7 -qGd//lycUXX3isoH5zUvj+G9e8AvfKtkqBf8yl17uuAh5XIuby6G2JVz2qwbU7lf -P9cZDSVP4WNjUYsLZD+tQ7ilHFw0s64AoGPF9n8LWWh4c6aMGKkCba/DGQEuuBDj -xsxAtGmjRjNph27Euxem8+jdrXO8ey8htf1mUQy9VLPhbV8cvCNz0QkDiRTSELlk -wyrQoZZKvOHUGlvHoMDBY3gPRDcwMpaAMiOVoXe6E9KXc+JdJclqDcM5YKS0sGlC -Qgnp2Ai8MyCzWCKnquvE4eZhg8XSlt/Z0E+t1wIDAQABAoIBAQCa0DQPUmIFUAHv -n+1kbsLE2hryhNeSEEiSxOlq64t1bMZ5OPLJckqGZFSVd8vDmp231B2kAMieTuTd -x7pnFsF0vKnWlI8rMBr77d8hBSPZSjm9mGtlmrjcxH3upkMVLj2+HSJgKnMw1T7Y -oqyGQy7E9WReP4l1DxHYUSVOn9iqo85gs+KK2X4b8GTKmlsFC1uqy+XjP24yIgXz -0PrvdFKB4l90073/MYNFdfpjepcu1rYZxpIm5CgGUFAOeC6peA0Ul7QS2DFAq6EB -QcIw+AdfFuRhd9Jg8p+N6PS662PeKpeB70xs5lU0USsoNPRTHMRYCj+7r7X3SoVD -LTzxWFiBAoGBAPIsVHY5I2PJEDK3k62vvhl1loFk5rW4iUJB0W3QHBv4G6xpyzY8 -ZH3c9Bm4w2CxV0hfUk9ZOlV/MsAZQ1A/rs5vF/MOn0DKTq0VO8l56cBZOHNwnAp8 -yTpIMqfYSXUKhcLC/RVz2pkJKmmanwpxv7AEpox6Wm9IWlQ7xrFTF9/nAoGBAMuT -3ncVXbdcXHzYkKmYLdZpDmOzo9ymzItqpKISjI57SCyySzfcBhh96v52odSh6T8N -zRtfr1+elltbD6F8r7ObkNtXczrtsCNErkFPHwdCEyNMy/r0FKTV9542fFufqDzB -hV900jkt/9CE3/uzIHoumxeu5roLrl9TpFLtG8SRAoGBAOyY2rvV/vlSSn0CVUlv -VW5SL4SjK7OGYrNU0mNS2uOIdqDvixWl0xgUcndex6MEH54ZYrUbG57D8rUy+UzB -qusMJn3UX0pRXKRFBnBEp1bA1CIUdp7YY1CJkNPiv4GVkjFBhzkaQwsYpVMfORpf -H0O8h2rfbtMiAP4imHBOGhkpAoGBAIpBVihRnl/Ungs7mKNU8mxW1KrpaTOFJAza -1AwtxL9PAmk4fNTm3Ezt1xYRwz4A58MmwFEC3rt1nG9WnHrzju/PisUr0toGakTJ -c/5umYf4W77xfOZltU9s8MnF/xbKixsX4lg9ojerAby/QM5TjI7t7+5ZneBj5nxe -9Y5L8TvBAoGATUX5QIzFW/QqGoq08hysa+kMVja3TnKW1eWK0uL/8fEYEz2GCbjY -dqfJHHFSlDBD4PF4dP1hG0wJzOZoKnGtHN9DvFbbpaS+NXCkXs9P/ABVmTo9I89n -WvUi+LUp0EQR6zUuRr79jhiyX6i/GTKh9dwD5nyaHwx8qbAOITc78bA= +MIIEowIBAAKCAQEAyiyM/Peu+8FL6cYaOFLnB/jsm2Nth54ytL0iy5yup2Rl00LF +aklYe75cqNcKPZOoJ/2q6xZGjmEIykXrC+dUvNkgaWszxuZz+UDMdjQ7esZfJilW +qIJNLXgtUwfwTvUBtkhU5IivLXU5vbZxYCtSO0TV3I13VguesnqjyN1IUGSTXc7I +0AjkSmv4KwJrxK8RRMKbo0ArK6LzdAUIfA10kExeM/cbMh8IA6qck2eLhhxU8Yrp +p/dnfW5LjiMBfQ66U1QAzjs0HVz0EL2PwD+FHBhogO54iy9QR/aKjZ0kUO/F/PH3 +Hr4ozpiPYQZcwemU9AzyEHGeI2xbin3MA7q5LwIDAQABAoIBAQDFmvXS62QkvbGt +NOu70Yvuxua8mlocDAwTjCnOSb6L7h14d/LtB/NsP4vhmw1vUjsxm0bLsGIWF9G3 +os8yO1EfpDmB0D4zUlxYa3Vss3DPd8TYT99bpMA6iRQD6+Z9xgt+VwRiuxY9oC5n +t0LpdG5Tb9x4Te0uNP1QBX7AfUiJCYd4ePpbe7WBgANDXdUGbo8V6sEVie939d43 +Cijk4Q1r3bg2uOfK/nEvCH2aFfgNTO+1+0t8gXFvqM+6DobACZXEPQfV04Gj1azI +VOZ3fq9l7rawJja+LWcrYGTgUN7NamcnZIj1tCNWGmjcXoIlMcxUyRwsjUm6AP1i +5ZmtU6sBAoGBAPBuKG/NERIaYEvJgsIAVvx81Ujlg2lVxPyqxltydCZzIRO8kpGy +AaavRovw0MmLMgD9V7PF2mipjV6BcQac0rebkyV9yYLV/yKIFeVaP7QB2BPjEnbp +SgQIt9fm11+pGMTDtqoCYCFAAN6P7xaX3Qz/OLcMwRhXzG/kmBArGShfAoGBANdE +LpnloB4qE6W3mE/gZrPKu7Tk5Bg/i5RTG1CslZ6BAVPI0izUdsoMUbn7Co1G9PRg +u6lFZg9J8Tvx8Q68fQS9oAFRS0OmOu8+aL/dtsPhMFN4XlxZ+Vra6x85wcZz8cdx +ZEm2e+tRFl40t+AgcQuG4zCJdGPgPFyvglCBImExAoGAYi35oT3yPJw8unX9SU9u +Ngib5/qhIQB/QlZSTcF9IL5ewXp9t7Ui63gjrL2X5NVMhA7wI18mAxtJuU/OYc7k +VUnYWrT09tKALw+3MUMbRFyEagqN3bUCHoeY2zdOt6eLj74D94Sk0K8cK8ZG8cjt +4YLPHCC/MTuZJhAI8IFI8q0CgYAft3QGSMbmqtxqNjrCyhVXuC8f3/mbeQFfwT7t +DACfcfJ4HcaZxFQcQORpuos7dZDx0K7Vqdv3tLVOV79kpHDnGelRSGEGfp+AUHmu +i0Q6aBtusPV2Net/b3HlD+V1D/A3qoVUNwbbDP92sd3FsAH36M/gfuAfNxKttU5F +/kDKgQKBgD4WHVDW9Ey0sZoW3srKYeDyN/C0AV8eXonFvdG9hqLqs67zk6/gg3YK +U0eCHiUGiI4JKzVPKtVLfEY65/Ig8HJ96O3FzZ37xVXqForRBY2rPZgreNLhDECL +K4hiAG1YPSuvbBYbMQCwyLbDPvobbFb3MhDc1TAkFDOcnc4PTEDd -----END RSA PRIVATE KEY----- From bf581a1a5e0dce07f9de70d1afd1bccf10d0e459 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 10:48:06 -0700 Subject: [PATCH 28/40] Enforce example TLS peer verification only when a CA is configured --- examples/mqttexample.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/mqttexample.c b/examples/mqttexample.c index b5c7c0af5..9abdd1c51 100644 --- a/examples/mqttexample.c +++ b/examples/mqttexample.c @@ -640,8 +640,14 @@ static int mqtt_tls_verify_cb(int preverify, WOLFSSL_X509_STORE_CTX* store) } return 1; #else - /* Propagate wolfSSL's chain-validation result so a bad certificate + /* With no CA configured there is no trust anchor to validate against + * (getting-started/demo mode), so accept and warn. When a CA is provided + * the chain-validation result is enforced so a bad certificate * (self-signed, expired, wrong host, untrusted CA) fails the handshake. */ + if (mqttCtx != NULL && mqttCtx->ca_file == NULL) { + PRINTF(" Warning: no CA configured, skipping server authentication"); + return 1; + } return preverify; #endif } From 94475051c301dab81c85bf711b77d77870df53af Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 10:48:06 -0700 Subject: [PATCH 29/40] Harden publish encode clamp, firmware length prints, and broker partial-write handling --- examples/firmware/fwclient.c | 19 +++++++++---------- src/mqtt_broker.c | 17 +++++++++++------ src/mqtt_packet.c | 2 +- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/examples/firmware/fwclient.c b/examples/firmware/fwclient.c index 3c70c75c0..8891d24e3 100644 --- a/examples/firmware/fwclient.c +++ b/examples/firmware/fwclient.c @@ -120,29 +120,28 @@ static int fw_message_process(MQTTCtx *mqttCtx, byte* buffer, word32 len) #endif word32 remaining; - /* Validate the field sizes sequentially against the received length. - * A summed length check (sizeof(header) + sigLen + pubKeyLen + fwLen) - * overflows word32 for attacker-chosen fwLen, letting a too-short buffer - * pass and leaving pubKeyBuf/fwBuf pointing past the allocation - * (CWE-190 -> heap OOB read/write). */ + /* Validate sequentially; a summed length check overflows word32 for + * attacker-chosen fwLen (CWE-190 -> heap OOB on pubKeyBuf/fwBuf). */ if (len < sizeof(FirmwareHeader)) { - PRINTF("Message smaller than firmware header! %d", len); + PRINTF("Message smaller than firmware header! %u", (unsigned int)len); return EXIT_FAILURE; } remaining = len - sizeof(FirmwareHeader); if (header->sigLen > remaining) { - PRINTF("Firmware sigLen exceeds message! %d", header->sigLen); + PRINTF("Firmware sigLen exceeds message! %u", + (unsigned int)header->sigLen); return EXIT_FAILURE; } remaining -= header->sigLen; if (header->pubKeyLen > remaining) { - PRINTF("Firmware pubKeyLen exceeds message! %d", header->pubKeyLen); + PRINTF("Firmware pubKeyLen exceeds message! %u", + (unsigned int)header->pubKeyLen); return EXIT_FAILURE; } remaining -= header->pubKeyLen; if (header->fwLen != remaining) { - PRINTF("Message header vs. actual size mismatch! %d != %d", - header->fwLen, remaining); + PRINTF("Message header vs. actual size mismatch! %u != %u", + (unsigned int)header->fwLen, (unsigned int)remaining); return EXIT_FAILURE; } diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 4e77b975b..ac661c5e4 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -5303,12 +5303,17 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, (unsigned)pub.total_len); wr = MqttPacket_Write(&sub->client->client, sub->client->tx_buf, sub_rc); - /* On a partial/non-blocking write the subscriber's - * write.pos is left mid-packet; reset it so the next - * fan-out does not resume a stale offset and desync - * this subscriber's stream. */ - if (wr != sub_rc) { - sub->client->client.write.pos = 0; + /* Static fan-out has no per-subscriber resume queue, so + * a partial write leaves this subscriber's stream + * desynced and unrecoverable. Tear down its socket; the + * main loop reaps it on the next read error. Removal is + * deferred so next_sub stays valid this iteration. */ + if (wr != sub_rc && + sub->client->sock != BROKER_SOCKET_INVALID) { + broker->net.close(broker->net.ctx, + sub->client->sock); + sub->client->sock = BROKER_SOCKET_INVALID; + sub->client->connected = 0; } } else { diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index 7506583a5..cfe93306b 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -1952,7 +1952,7 @@ int MqttEncode_Publish(byte *tx_buf, int tx_buf_len, MqttPublish *publish, * decoded buffer_len; clamping here prevents an OOB read past the * source buffer during fan-out. */ if (publish->buffer_len > 0 && - payload_len > (int)publish->buffer_len) { + publish->buffer_len < (word32)payload_len) { payload_len = (int)publish->buffer_len; } if (tx_payload != NULL) { From 3004ba98388b13f7f20e664557d97f47aaeb6635 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 11:06:45 -0700 Subject: [PATCH 30/40] F-4927 - Enforce curl TLS hostname verification instead of bypassing for localhost --- examples/mqttnet.c | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/examples/mqttnet.c b/examples/mqttnet.c index 556497bba..19450be56 100644 --- a/examples/mqttnet.c +++ b/examples/mqttnet.c @@ -856,14 +856,13 @@ mqttcurl_connect(SocketContext* sock, const char* host, word16 port, return MQTT_CODE_ERROR_CURL; } - /* Only do server host verification when not running against - * localhost broker. */ - if (XSTRCMP(host, "localhost") == 0) { - res = curl_easy_setopt(sock->curl, CURLOPT_SSL_VERIFYHOST, 0L); - } - else { - res = curl_easy_setopt(sock->curl, CURLOPT_SSL_VERIFYHOST, 2L); - } + /* Enforce server hostname verification. The cert's CN/SAN must match + * the connect host (broker_test certs carry a localhost SAN). */ +#ifdef WOLFMQTT_ALLOW_INSECURE_TLS + res = curl_easy_setopt(sock->curl, CURLOPT_SSL_VERIFYHOST, 0L); +#else + res = curl_easy_setopt(sock->curl, CURLOPT_SSL_VERIFYHOST, 2L); +#endif if (res != CURLE_OK) { PRINTF("error: curl_easy_setopt(SSL_VERIFYHOST) returned: %d", From c138692aec62826bc9f02daf9f9ba5cd463dcae9 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 11:14:31 -0700 Subject: [PATCH 31/40] Defer nested retained-delivery reap to outermost depth to avoid use-after-free --- src/mqtt_broker.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index ac661c5e4..9f454d68b 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -3797,11 +3797,19 @@ static void BrokerRetained_DeliverToClient(MqttBroker* broker, rm = broker->retained; while (rm) { BrokerRetainedMsg* rm_next = rm->next; - /* Reap expired messages and any nodes a re-entrant delete deferred - * (pending_delete). The free here is safe: rm_next is already saved. */ + /* Reap deferred-delete and expired nodes. Freeing is only safe at the + * outermost delivery depth; a nested re-entrant (WS fan-out) delivery + * may hold rm in an enclosing loop's saved rm_next, so at deeper depths + * mark the node and defer to the retained_delivering==0 post-loop reap. */ if (rm->pending_delete || (rm->expiry_sec > 0 && (now - rm->store_time) >= rm->expiry_sec)) { + if (broker->retained_delivering > 1) { + rm->pending_delete = 1; + rm_prev = rm; + rm = rm_next; + continue; + } WBLOG_DBG(broker, "broker: retained expired topic=%s", BrokerLog_Sanitize(rm->topic)); if (rm_prev) { From 8e5ee6ca7a35e14ff471ba1781d0210bec8af614 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Mon, 15 Jun 2026 09:38:06 -0700 Subject: [PATCH 32/40] Drop CWE and internal issue references from comments and trim firmware topic comment --- examples/aws/awsiot.c | 2 +- examples/firmware/fwclient.c | 7 ++----- examples/mqtt_log.h | 2 +- examples/websocket/net_libwebsockets.c | 2 +- src/mqtt_broker.c | 4 ++-- src/mqtt_packet.c | 2 +- src/mqtt_sn_packet.c | 4 ++-- tests/test_broker_connect.c | 6 +++--- tests/test_mqtt_client.c | 2 +- tests/test_mqtt_packet.c | 4 ++-- tests/test_mqtt_sn.c | 14 +++++++------- tests/test_mqtt_sn_client.c | 8 ++++---- wolfmqtt/mqtt_broker.h | 2 +- 13 files changed, 28 insertions(+), 31 deletions(-) diff --git a/examples/aws/awsiot.c b/examples/aws/awsiot.c index 5d1c4b547..7103441b8 100644 --- a/examples/aws/awsiot.c +++ b/examples/aws/awsiot.c @@ -405,7 +405,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, least one property. The property list is deallocated after returning from the callback. */ /* Copy a length-delimited, broker-controlled property string into dst and - * sanitize it for safe logging (CWE-117). */ + * sanitize it for safe logging. */ static const char* awsiot_log_prop(char* dst, word32 dstLen, const char* src, word32 srcLen) { diff --git a/examples/firmware/fwclient.c b/examples/firmware/fwclient.c index 8891d24e3..d1b889961 100644 --- a/examples/firmware/fwclient.c +++ b/examples/firmware/fwclient.c @@ -120,8 +120,7 @@ static int fw_message_process(MQTTCtx *mqttCtx, byte* buffer, word32 len) #endif word32 remaining; - /* Validate sequentially; a summed length check overflows word32 for - * attacker-chosen fwLen (CWE-190 -> heap OOB on pubKeyBuf/fwBuf). */ + /* Validate field sizes sequentially; a summed length check can overflow. */ if (len < sizeof(FirmwareHeader)) { PRINTF("Message smaller than firmware header! %u", (unsigned int)len); return EXIT_FAILURE; @@ -189,9 +188,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, { MQTTCtx* mqttCtx = (MQTTCtx*)client->ctx; - /* Verify this message is for the firmware topic. Compare against the full - * expected length (not the wire-supplied topic_name_len) so a zero-length - * (v5 Topic Alias) or byte-prefix topic cannot pass the gate. */ + /* Verify this message is for the firmware topic */ if (msg_new && msg->topic_name_len == (word16)XSTRLEN(mqttCtx->topic_name) && XSTRNCMP(msg->topic_name, mqttCtx->topic_name, diff --git a/examples/mqtt_log.h b/examples/mqtt_log.h index 06ae8a6a6..2d6d8ee23 100644 --- a/examples/mqtt_log.h +++ b/examples/mqtt_log.h @@ -40,7 +40,7 @@ extern "C" { #define MQTT_LOG_MAYBE_UNUSED #endif -/* Sanitize an untrusted string for safe logging (CWE-117 defense). +/* Sanitize an untrusted string for safe logging. * * MQTT/MQTT-SN strings such as a REGISTER topic name are controlled by the * remote peer. For MQTT-SN this peer is reachable over UDP, so an attacker who diff --git a/examples/websocket/net_libwebsockets.c b/examples/websocket/net_libwebsockets.c index 35deefaa4..22d4bab1e 100644 --- a/examples/websocket/net_libwebsockets.c +++ b/examples/websocket/net_libwebsockets.c @@ -73,7 +73,7 @@ static int callback_mqtt(struct lws *wsi, enum lws_callback_reasons reason, net->status = 0; /* libwebsockets frees the wsi after this callback returns; clear the * dangling pointer so NetWebsocket_Disconnect's `if (net->wsi)` guard - * skips lws_close_reason() on freed memory (CWE-416). */ + * skips lws_close_reason() on freed memory. */ net->wsi = NULL; } else if (reason == LWS_CALLBACK_CLIENT_RECEIVE) { diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 9f454d68b..c1439e11d 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -121,7 +121,7 @@ static void MqttBroker_ForceZero(void* mem, word32 len) #define BROKER_LOG_SAN_POOL 4 /* distinct buffers per log statement */ /* Sanitize a peer-controlled string (topic, filter, client_id, cert CN, ...) - * before it reaches a PRINTF log sink (CWE-117). Control bytes (< 0x20 and + * before it reaches a PRINTF log sink. Control bytes (< 0x20 and * DEL 0x7f) become printable escapes so a remote peer cannot inject forged log * lines (CR/LF) or hijack the operator terminal (ANSI ESC). The result is * returned from a small rotating pool of static buffers so several sanitized @@ -3330,7 +3330,7 @@ static void BrokerRetained_Delete(MqttBroker* broker, const char* topic) if (broker->retained_delivering > 0) { /* A delivery loop is iterating this list (possibly re-entered * via a WebSocket fan-out). Freeing now would invalidate that - * loop's saved next pointer (CWE-416); flag for deferred reap + * loop's saved next pointer; flag for deferred reap * by the delivery loop instead. */ cur->pending_delete = 1; found = 1; diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index cfe93306b..69b885b9d 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -842,7 +842,7 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf, while (((int)prop_len > 0) && (rc >= 0)) { /* Bound the number of properties a single message may carry so a peer - * cannot saturate the shared property pool (CWE-770). */ + * cannot saturate the shared property pool. */ if (++prop_count > MQTT_MAX_PROPS) { rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PROPERTY); break; diff --git a/src/mqtt_sn_packet.c b/src/mqtt_sn_packet.c index 3205e2377..4236c3961 100644 --- a/src/mqtt_sn_packet.c +++ b/src/mqtt_sn_packet.c @@ -198,7 +198,7 @@ int SN_Decode_Header(byte *rx_buf, int rx_buf_len, /* Bytes the declared packet leaves for the MsgId at * rx_buf + id_offset. Bound the read by total_len (the declared * packet length, already validated <= rx_buf_len above), not by - * rx_buf_len: this keeps the read inside the buffer (CWE-125) and + * rx_buf_len: this keeps the read inside the buffer and * additionally rejects a frame whose declared length stops short * of the MsgId rather than reading adjacent bytes. Evaluate as a * signed int and reject before the unsigned cast below, so a short @@ -1332,7 +1332,7 @@ int SN_Decode_Publish(byte *rx_buf, int rx_buf_len, SN_Publish *publish) * MsgId so the matching PUBACK/PUBREC can be correlated. Reject MsgId=0 * here; otherwise SN_Client_HandlePacket would emit a response carrying * MsgId=0 that no conformant gateway can match, leaving its retransmit - * timer to replay the same message (CWE-20). Mirrors the standard MQTT + * timer to replay the same message. Mirrors the standard MQTT * decoder guard in mqtt_packet.c. QoS 0 and QoS -1 (MQTT_QOS_3, the * connectionless publish) send no response and legitimately use MsgId=0, * so they are intentionally excluded. */ diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index 925d0e681..483519fdf 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -590,7 +590,7 @@ TEST(connect_v311_binary_password_exact_match_accepted) MqttBroker_Free(&broker); } -/* [CWE-863/CWE-639] An unauthenticated CONNECT must not mutate another +/* An unauthenticated CONNECT must not mutate another * client's session. A victim authenticates and stays connected; an attacker * then reuses the victim's client_id with a wrong password. The broker must * reject the attacker at the credential gate BEFORE the duplicate-takeover @@ -1676,7 +1676,7 @@ TEST(broker_publish_before_connect_closes) } #if defined(WOLFMQTT_BROKER_RETAINED) && !defined(WOLFMQTT_STATIC_MEMORY) -/* [CWE-400] The dynamic retained-message list must be bounded. A client that +/* The dynamic retained-message list must be bounded. A client that * publishes RETAIN=1 to more than BROKER_MAX_RETAINED distinct topics must not * grow the list past the cap - pre-fix it grew without bound, enabling * heap-exhaustion DoS. */ @@ -1724,7 +1724,7 @@ TEST(broker_retained_list_capped) #endif /* WOLFMQTT_BROKER_RETAINED && !WOLFMQTT_STATIC_MEMORY */ #ifndef WOLFMQTT_STATIC_MEMORY -/* [CWE-770] A single client cannot occupy more than BROKER_MAX_SUBS_PER_CLIENT +/* A single client cannot occupy more than BROKER_MAX_SUBS_PER_CLIENT * slots in the shared subscription table; excess SUBSCRIBEs are refused so * other clients are not denied service. */ TEST(broker_per_client_subscription_cap) diff --git a/tests/test_mqtt_client.c b/tests/test_mqtt_client.c index e924775ad..58d739336 100644 --- a/tests/test_mqtt_client.c +++ b/tests/test_mqtt_client.c @@ -407,7 +407,7 @@ static int mock_net_read_canned(void *context, byte* buf, int buf_len, return n; } -/* [issue 3129] A broker that rejects a subscription returns a SUBACK whose +/* A broker that rejects a subscription returns a SUBACK whose * per-topic return code has the high bit set (0x80 in v3.1.1, any reason * code >= 0x80 in v5). MqttClient_Subscribe must surface this as * MQTT_CODE_ERROR_SUBSCRIBE_REJECTED rather than MQTT_CODE_SUCCESS, else the diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index f3ec03fc9..0963fe9a5 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -772,7 +772,7 @@ TEST(encode_publish_qos1_valid) ASSERT_TRUE(rc > 0); } -/* [CWE-125] The encoder must clamp the copied payload to buffer_len so a +/* The encoder must clamp the copied payload to buffer_len so a * total_len larger than the bytes actually present cannot read past the * source buffer (the broker fan-out OOB read). */ TEST(encode_publish_clamps_payload_to_buffer_len) @@ -1309,7 +1309,7 @@ TEST(decode_publish_v5_response_topic_wildcard_rejected) ASSERT_NULL(pub.props); } -/* [CWE-770] A single message may not carry more than the internal +/* A single message may not carry more than the internal * MQTT_MAX_PROPS (default 30) properties; otherwise a peer can saturate the * shared property pool. 40 User Property entries exceeds that cap. The cap * returns MQTT_CODE_ERROR_PROPERTY, distinct from the MQTT_CODE_ERROR_MEMORY a diff --git a/tests/test_mqtt_sn.c b/tests/test_mqtt_sn.c index d162c6e0f..21f1ab075 100644 --- a/tests/test_mqtt_sn.c +++ b/tests/test_mqtt_sn.c @@ -48,7 +48,7 @@ /* Log-sanitization helper shared by the SN example clients. Exercised here * because the MQTT-SN REGISTER topic name decoded above is aliased straight * from the receive buffer (gateway-/attacker-controlled over UDP) and is fed - * to a PRINTF sink by sn_reg_callback; this helper is the CWE-117 defense. */ + * to a PRINTF sink by sn_reg_callback; this helper is the log defense. */ #include "examples/mqtt_log.h" /* PRINT_BUFFER_SIZE is the payload clamp the SN example clients apply before @@ -546,7 +546,7 @@ TEST(sn_register_no_room_for_terminator_rejected) TEST(sn_register_roundtrip_short_form) { - /* Report 3831: encode then decode a normal REGISTER and confirm every + /* Encode then decode a normal REGISTER and confirm every * field survives the round trip. "sensors/temp" is 12 bytes, so * total_len = 12 + 6 = 18 (<= 255 -> short, single-byte length). The decode * buffer is larger than the packet so the in-place NUL terminator fits. */ @@ -574,7 +574,7 @@ TEST(sn_register_roundtrip_short_form) TEST(sn_register_roundtrip_ind_form) { - /* Report 3831: same round trip but with an extended-length (IND) encoding. + /* Same round trip but with an extended-length (IND) encoding. * A 260-byte topic gives total_len = 260 + 6 = 266 (> 255), so the encoder * switches to the 3-byte length header (IND + 2 length bytes) and * total_len becomes 268. Confirms the decoder honors the IND form and that @@ -606,9 +606,9 @@ TEST(sn_register_roundtrip_ind_form) } /* ============================================================================ - * mqtt_log_sanitize (CWE-117 log-injection defense) + * mqtt_log_sanitize (log-injection defense) * - * Report 5663: the MQTT-SN REGISTER topic name decoded by SN_Decode_Register is + * The MQTT-SN REGISTER topic name decoded by SN_Decode_Register is * aliased straight from the UDP receive buffer with no control-character * filtering, then logged verbatim by sn_reg_callback's PRINTF. A spoofed * gateway can therefore inject forged log lines (CR/LF) or hijack the terminal @@ -1149,7 +1149,7 @@ TEST(sn_encode_publish_null_args_rejected) TEST(sn_decode_publish_reserved_topic_type_rejected) { - /* Report 4659 PoC: flags=0x03 sets the reserved topic id type 0b11. The + /* flags=0x03 sets the reserved topic id type 0b11. The * pre-fix decoder returned 7 (success) with topic_type=3. */ byte buf[7] = { 0x07, 0x0C, 0x03, 0x00, 0x01, 0x00, 0x00 }; SN_Publish publish; @@ -1239,7 +1239,7 @@ TEST(sn_decode_publish_null_args_rejected) TEST(sn_decode_publish_qos1_zero_packet_id_rejected) { - /* Report 4248 PoC: flags=0x20 -> QoS1, MsgId=0x0000. */ + /* flags=0x20 -> QoS1, MsgId=0x0000. */ byte buf[7] = { 0x07, 0x0C, 0x20, 0x00, 0x01, 0x00, 0x00 }; SN_Publish publish; int rc; diff --git a/tests/test_mqtt_sn_client.c b/tests/test_mqtt_sn_client.c index ed42b8192..55d491c82 100644 --- a/tests/test_mqtt_sn_client.c +++ b/tests/test_mqtt_sn_client.c @@ -649,7 +649,7 @@ TEST(sn_willmsgupd_payload_scrubbed_on_write_error) } /* ============================================================================ - * SN subscribe pending-response lifecycle tests (CWE-416 regression, #5864) + * SN subscribe pending-response lifecycle tests * * SN_Client_Subscribe registers &subscribe->pendResp in client->firstPendResp * (MULTITHREAD) and must remove it exactly once the SUBACK arrives. Under @@ -717,7 +717,7 @@ TEST(sn_subscribe_rejected) } /* ============================================================================ - * SN publish pending-response lifecycle tests (CWE-416 regression, #5146) + * SN publish pending-response lifecycle tests * * For QoS 1/2 SN_Client_Publish registers &publish->pendResp in * client->firstPendResp (MULTITHREAD) and must remove it exactly once the @@ -922,7 +922,7 @@ TEST(sn_willmsgupd_payload_scrubbed_nonblock) sn_will_msg_update_scrub_check(1 /* one CONTINUE per frame */); } -/* The headline #5864 regression. With a CONTINUE armed before the SUBACK the +/* The headline regression. With a CONTINUE armed before the SUBACK the * subscribe must converge to SUCCESS and remove its pending response exactly * once. Under MULTITHREAD it also pins the dangling-pointer contract directly: * after the first in-flight CONTINUE the entry IS linked and points back into @@ -1024,7 +1024,7 @@ TEST(sn_subscribe_rejected_nonblock) ASSERT_NO_PENDRESP(); } -/* The headline #5146 regression. With a CONTINUE armed before the PUBACK the +/* The headline regression. With a CONTINUE armed before the PUBACK the * QoS 1 publish must converge to SUCCESS and remove its pending response exactly * once. Under MULTITHREAD it also pins the dangling-pointer contract directly: * after the first in-flight CONTINUE the entry IS linked and points back into diff --git a/wolfmqtt/mqtt_broker.h b/wolfmqtt/mqtt_broker.h index 9f04f0d67..2242b6cdd 100644 --- a/wolfmqtt/mqtt_broker.h +++ b/wolfmqtt/mqtt_broker.h @@ -94,7 +94,7 @@ #define BROKER_MAX_SUBS 32 #endif /* Per-client subscription cap so one client cannot occupy the whole shared - * subscription table and deny other clients (CWE-770). */ + * subscription table and deny other clients. */ #ifndef BROKER_MAX_SUBS_PER_CLIENT /* At least 1 so a config where BROKER_MAX_CLIENTS > BROKER_MAX_SUBS does * not collapse the cap to 0 and reject every subscription. */ From cc4de83c6fa19f817099c4691ecdc39b51bf88b4 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Mon, 15 Jun 2026 09:38:06 -0700 Subject: [PATCH 33/40] Regenerate broker test certs as X.509 v3 with localhost SAN --- scripts/broker_test/ca-cert.pem | 135 +++++++++-------- scripts/broker_test/server-cert.pem | 216 ++++++++++++---------------- scripts/broker_test/server-key.pem | 50 +++---- 3 files changed, 193 insertions(+), 208 deletions(-) diff --git a/scripts/broker_test/ca-cert.pem b/scripts/broker_test/ca-cert.pem index 4a7ac6a6e..d172c8936 100644 --- a/scripts/broker_test/ca-cert.pem +++ b/scripts/broker_test/ca-cert.pem @@ -1,72 +1,93 @@ Certificate: Data: - Version: 1 (0x0) + Version: 3 (0x2) Serial Number: - cb:f5:0d:cd:48:75:df:5e + b8:a8:ef:9d:53:16:38:ab Signature Algorithm: sha256WithRSAEncryption Issuer: C=US, ST=Montana, L=Bozeman, O=Sawtooth, OU=Consulting, CN=www.wolfssl.com/emailAddress=info@wolfssl.com Validity - Not Before: Jun 12 17:30:11 2026 GMT - Not After : Jun 9 17:30:11 2036 GMT + Not Before: Jun 15 16:36:44 2026 GMT + Not After : Jun 12 16:36:44 2036 GMT Subject: C=US, ST=Montana, L=Bozeman, O=Sawtooth, OU=Consulting, CN=www.wolfssl.com/emailAddress=info@wolfssl.com Subject Public Key Info: Public Key Algorithm: rsaEncryption RSA Public-Key: (2048 bit) Modulus: - 00:ce:6b:43:26:a4:36:e0:da:0b:ee:4b:84:5f:72: - df:fe:22:76:34:44:6c:c9:45:8c:8b:be:8b:f2:af: - 67:aa:74:25:48:77:e6:44:6c:80:69:9e:ae:d2:ec: - 5c:c3:e3:f2:51:4a:6c:c9:c2:c9:cb:5e:6d:74:d9: - 28:08:e9:6d:2e:6e:5b:7b:60:b8:17:cf:90:fc:dc: - 0b:fc:8f:e8:e9:4a:6f:e8:28:7d:50:d4:f3:a1:ea: - 29:62:92:15:82:f4:3b:74:88:6d:6f:ce:a5:63:bf: - 13:fc:34:03:62:ab:c7:24:6e:4e:62:69:88:6a:7f: - af:89:78:2c:64:88:0a:6e:59:b7:a1:43:43:ce:f3: - db:7a:97:af:f2:15:d1:e2:59:ea:65:8e:94:14:92: - 16:45:d6:d0:11:c1:5d:b9:04:00:96:6b:f3:cc:e6: - bb:ab:37:f7:d8:8d:29:a1:b0:d0:cc:dd:60:50:90: - 1d:f1:8d:b0:c9:01:6a:49:d2:b0:df:79:37:3c:b0: - c5:a0:4b:54:36:a0:b0:aa:c4:a1:09:66:6a:3a:07: - a6:42:d4:83:b1:9a:30:e2:8d:fd:41:01:86:2c:6a: - bf:41:5d:d6:37:20:34:9d:2b:be:de:0f:d7:d9:4e: - fe:e0:97:53:96:02:62:f3:87:55:de:47:bc:83:26: - dd:29 + 00:bd:af:93:11:5d:d0:f7:38:60:11:7f:4a:57:bc: + b6:9a:29:f4:7e:c8:8d:aa:1c:7c:c3:96:93:9b:38: + 1a:e5:04:c0:5d:ba:cd:65:d3:79:c5:64:0b:9d:e8: + 35:94:1f:0e:d8:96:51:99:56:76:86:ef:63:85:ea: + c1:84:9b:b7:83:1a:00:6b:6c:1b:c3:7e:1c:ce:45: + 28:7e:81:64:cd:4c:c3:cc:94:50:c9:5c:54:c8:8f: + 7b:7d:bf:55:0a:7e:33:45:19:83:cb:e3:53:9c:55: + 4a:03:ae:42:fe:90:73:0c:36:89:e4:82:bd:c4:ae: + af:d4:56:70:ce:f7:01:29:96:02:ff:68:72:52:57: + 21:3d:4b:52:36:bb:99:80:02:95:30:16:e9:d4:2f: + 6f:89:63:e7:db:d9:ad:7f:02:2d:8a:8f:f6:d3:32: + 69:38:67:89:c6:0d:4d:f1:e2:e0:c9:ef:10:96:ae: + ae:b2:46:3c:89:39:79:37:85:5f:0f:52:83:f5:cc: + 05:17:67:60:52:64:1c:99:96:4d:6f:f9:17:c6:b9: + a5:bc:6e:8f:a6:65:16:c0:db:0e:7b:62:c5:ca:cc: + 49:86:56:38:4b:b7:aa:fe:ea:45:36:09:20:5e:e9: + 52:7a:52:a0:4b:cd:19:8c:ef:c0:3b:2c:98:bb:75: + bb:ad Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Subject Key Identifier: + 39:6C:89:67:B2:33:EE:AF:67:61:74:DE:9A:C2:CF:DA:4F:6D:CD:40 + X509v3 Authority Key Identifier: + keyid:39:6C:89:67:B2:33:EE:AF:67:61:74:DE:9A:C2:CF:DA:4F:6D:CD:40 + DirName:/C=US/ST=Montana/L=Bozeman/O=Sawtooth/OU=Consulting/CN=www.wolfssl.com/emailAddress=info@wolfssl.com + serial:B8:A8:EF:9D:53:16:38:AB + + X509v3 Key Usage: critical + Certificate Sign, CRL Sign + X509v3 Subject Alternative Name: + DNS:example.com, IP Address:127.0.0.1 Signature Algorithm: sha256WithRSAEncryption - 57:a9:91:4b:3c:fe:1e:b3:58:58:07:82:7f:a8:33:9b:e6:2f: - f5:e6:de:d0:72:00:1a:04:6f:15:cc:01:8b:10:1f:19:e3:d2: - 8e:d9:27:48:6c:2d:ef:c2:1e:a3:97:fe:3f:bd:cf:97:5a:1b: - 85:19:20:63:e6:2a:85:6d:08:cb:b8:38:c1:38:3f:c9:88:90: - e2:48:f4:c4:22:d2:3f:78:63:f0:a8:1b:26:08:2b:23:82:2c: - f0:e9:51:21:65:58:87:ad:73:03:41:95:67:ff:e9:b9:3d:93: - 19:49:ff:39:6a:75:c6:74:3b:e9:51:d0:07:28:14:30:f9:1a: - b2:23:63:b6:25:64:f0:3a:62:bf:84:40:65:ea:38:6c:f6:86: - 13:fb:94:98:ac:03:37:6e:e5:75:28:65:20:69:b2:f9:9a:2c: - e6:af:ad:74:78:d8:5f:f4:d2:6c:4d:32:6c:3a:ab:65:6b:d7: - 3c:f9:fb:6a:18:fb:c0:88:e4:40:0b:38:cd:d4:5a:1c:b0:bb: - 18:3a:6a:18:20:0f:27:0e:ea:38:13:50:2c:f4:3a:00:dc:e9: - 1a:d5:52:21:cb:3c:7c:e1:25:e8:d5:e1:b4:2e:79:2c:45:8a: - cb:63:90:60:57:96:a0:17:6f:d8:cf:05:76:b1:51:12:86:a3: - 58:36:00:5c + 4b:52:2f:78:9d:36:de:31:d9:f9:17:25:3a:f2:b3:d5:7d:f9: + 36:87:0d:64:59:3c:21:cd:5d:58:b1:38:86:37:fd:f2:f9:77: + d7:45:f9:aa:db:81:0d:a9:62:38:14:6c:d3:26:2f:59:ed:8a: + 42:0d:ba:55:e2:93:25:b3:b9:c2:36:61:4c:de:e2:53:f3:a6: + d4:ba:d4:ca:b1:40:86:6b:60:55:8e:89:50:86:92:ca:62:62: + 36:a4:0b:e2:ef:ca:31:76:cd:dd:9f:4d:20:a6:4b:28:96:36: + fa:d5:b1:80:be:9e:df:81:2f:cf:52:98:0b:63:f9:de:fa:ea: + 41:bf:5e:43:0b:5e:55:15:a5:72:07:ab:49:c6:54:fb:c7:48: + ba:a4:31:fe:fa:e1:c7:dd:0c:c2:e8:78:10:04:a6:bd:fe:88: + 00:18:5a:d7:d4:a4:58:f8:c5:97:03:16:dc:9c:d1:ba:d1:94: + 61:87:b3:5e:45:31:40:09:21:71:ce:e9:94:fc:53:29:78:78: + a6:c6:a1:be:3e:55:6f:8e:7c:cd:8f:3f:1a:d8:d3:ac:24:57: + 34:51:2a:7d:da:a6:28:bb:e3:75:0d:8f:e3:e8:b5:32:4e:db: + 0e:c9:e3:62:5c:fd:43:2d:08:7f:25:73:83:4e:a6:fd:76:a3: + 51:d9:16:2c -----BEGIN CERTIFICATE----- -MIIDpjCCAo4CCQDL9Q3NSHXfXjANBgkqhkiG9w0BAQsFADCBlDELMAkGA1UEBhMC -VVMxEDAOBgNVBAgMB01vbnRhbmExEDAOBgNVBAcMB0JvemVtYW4xETAPBgNVBAoM -CFNhd3Rvb3RoMRMwEQYDVQQLDApDb25zdWx0aW5nMRgwFgYDVQQDDA93d3cud29s -ZnNzbC5jb20xHzAdBgkqhkiG9w0BCQEWEGluZm9Ad29sZnNzbC5jb20wHhcNMjYw -NjEyMTczMDExWhcNMzYwNjA5MTczMDExWjCBlDELMAkGA1UEBhMCVVMxEDAOBgNV -BAgMB01vbnRhbmExEDAOBgNVBAcMB0JvemVtYW4xETAPBgNVBAoMCFNhd3Rvb3Ro -MRMwEQYDVQQLDApDb25zdWx0aW5nMRgwFgYDVQQDDA93d3cud29sZnNzbC5jb20x -HzAdBgkqhkiG9w0BCQEWEGluZm9Ad29sZnNzbC5jb20wggEiMA0GCSqGSIb3DQEB -AQUAA4IBDwAwggEKAoIBAQDOa0MmpDbg2gvuS4Rfct/+InY0RGzJRYyLvovyr2eq -dCVId+ZEbIBpnq7S7FzD4/JRSmzJwsnLXm102SgI6W0ublt7YLgXz5D83Av8j+jp -Sm/oKH1Q1POh6ilikhWC9Dt0iG1vzqVjvxP8NANiq8ckbk5iaYhqf6+JeCxkiApu -WbehQ0PO89t6l6/yFdHiWepljpQUkhZF1tARwV25BACWa/PM5rurN/fYjSmhsNDM -3WBQkB3xjbDJAWpJ0rDfeTc8sMWgS1Q2oLCqxKEJZmo6B6ZC1IOxmjDijf1BAYYs -ar9BXdY3IDSdK77eD9fZTv7gl1OWAmLzh1XeR7yDJt0pAgMBAAEwDQYJKoZIhvcN -AQELBQADggEBAFepkUs8/h6zWFgHgn+oM5vmL/Xm3tByABoEbxXMAYsQHxnj0o7Z -J0hsLe/CHqOX/j+9z5daG4UZIGPmKoVtCMu4OME4P8mIkOJI9MQi0j94Y/CoGyYI -KyOCLPDpUSFlWIetcwNBlWf/6bk9kxlJ/zlqdcZ0O+lR0AcoFDD5GrIjY7YlZPA6 -Yr+EQGXqOGz2hhP7lJisAzdu5XUoZSBpsvmaLOavrXR42F/00mxNMmw6q2Vr1zz5 -+2oY+8CI5EALOM3UWhywuxg6ahggDycO6jgTUCz0OgDc6RrVUiHLPHzhJejV4bQu -eSxFistjkGBXlqAXb9jPBXaxURKGo1g2AFw= +MIIE3TCCA8WgAwIBAgIJALio751TFjirMA0GCSqGSIb3DQEBCwUAMIGUMQswCQYD +VQQGEwJVUzEQMA4GA1UECAwHTW9udGFuYTEQMA4GA1UEBwwHQm96ZW1hbjERMA8G +A1UECgwIU2F3dG9vdGgxEzARBgNVBAsMCkNvbnN1bHRpbmcxGDAWBgNVBAMMD3d3 +dy53b2xmc3NsLmNvbTEfMB0GCSqGSIb3DQEJARYQaW5mb0B3b2xmc3NsLmNvbTAe +Fw0yNjA2MTUxNjM2NDRaFw0zNjA2MTIxNjM2NDRaMIGUMQswCQYDVQQGEwJVUzEQ +MA4GA1UECAwHTW9udGFuYTEQMA4GA1UEBwwHQm96ZW1hbjERMA8GA1UECgwIU2F3 +dG9vdGgxEzARBgNVBAsMCkNvbnN1bHRpbmcxGDAWBgNVBAMMD3d3dy53b2xmc3Ns +LmNvbTEfMB0GCSqGSIb3DQEJARYQaW5mb0B3b2xmc3NsLmNvbTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAL2vkxFd0Pc4YBF/Sle8tpop9H7IjaocfMOW +k5s4GuUEwF26zWXTecVkC53oNZQfDtiWUZlWdobvY4XqwYSbt4MaAGtsG8N+HM5F +KH6BZM1Mw8yUUMlcVMiPe32/VQp+M0UZg8vjU5xVSgOuQv6Qcww2ieSCvcSur9RW +cM73ASmWAv9oclJXIT1LUja7mYAClTAW6dQvb4lj59vZrX8CLYqP9tMyaThnicYN +TfHi4MnvEJaurrJGPIk5eTeFXw9Sg/XMBRdnYFJkHJmWTW/5F8a5pbxuj6ZlFsDb +DntixcrMSYZWOEu3qv7qRTYJIF7pUnpSoEvNGYzvwDssmLt1u60CAwEAAaOCAS4w +ggEqMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFDlsiWeyM+6vZ2F03prCz9pP +bc1AMIHJBgNVHSMEgcEwgb6AFDlsiWeyM+6vZ2F03prCz9pPbc1AoYGapIGXMIGU +MQswCQYDVQQGEwJVUzEQMA4GA1UECAwHTW9udGFuYTEQMA4GA1UEBwwHQm96ZW1h +bjERMA8GA1UECgwIU2F3dG9vdGgxEzARBgNVBAsMCkNvbnN1bHRpbmcxGDAWBgNV +BAMMD3d3dy53b2xmc3NsLmNvbTEfMB0GCSqGSIb3DQEJARYQaW5mb0B3b2xmc3Ns +LmNvbYIJALio751TFjirMA4GA1UdDwEB/wQEAwIBBjAcBgNVHREEFTATggtleGFt +cGxlLmNvbYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAS1IveJ023jHZ+RclOvKz +1X35NocNZFk8Ic1dWLE4hjf98vl310X5qtuBDaliOBRs0yYvWe2KQg26VeKTJbO5 +wjZhTN7iU/Om1LrUyrFAhmtgVY6JUIaSymJiNqQL4u/KMXbN3Z9NIKZLKJY2+tWx +gL6e34Evz1KYC2P53vrqQb9eQwteVRWlcgerScZU+8dIuqQx/vrhx90Mwuh4EASm +vf6IABha19SkWPjFlwMW3JzRutGUYYezXkUxQAkhcc7plPxTKXh4psahvj5Vb458 +zY8/GtjTrCRXNFEqfdqmKLvjdQ2P4+i1Mk7bDsnjYlz9Qy0IfyVzg06m/XajUdkW +LA== -----END CERTIFICATE----- diff --git a/scripts/broker_test/server-cert.pem b/scripts/broker_test/server-cert.pem index ecb280af4..950f369ca 100644 --- a/scripts/broker_test/server-cert.pem +++ b/scripts/broker_test/server-cert.pem @@ -2,154 +2,118 @@ Certificate: Data: Version: 3 (0x2) Serial Number: - c5:e2:b6:41:60:d5:38:e9 + fb:42:38:d1:2f:26:06:bd Signature Algorithm: sha256WithRSAEncryption Issuer: C=US, ST=Montana, L=Bozeman, O=Sawtooth, OU=Consulting, CN=www.wolfssl.com/emailAddress=info@wolfssl.com Validity - Not Before: Jun 12 17:30:11 2026 GMT - Not After : Jun 9 17:30:11 2036 GMT + Not Before: Jun 15 16:36:44 2026 GMT + Not After : Jun 12 16:36:44 2036 GMT Subject: C=US, ST=Montana, L=Bozeman, O=wolfSSL, OU=Support, CN=www.wolfssl.com/emailAddress=info@wolfssl.com Subject Public Key Info: Public Key Algorithm: rsaEncryption RSA Public-Key: (2048 bit) Modulus: - 00:ca:2c:8c:fc:f7:ae:fb:c1:4b:e9:c6:1a:38:52: - e7:07:f8:ec:9b:63:6d:87:9e:32:b4:bd:22:cb:9c: - ae:a7:64:65:d3:42:c5:6a:49:58:7b:be:5c:a8:d7: - 0a:3d:93:a8:27:fd:aa:eb:16:46:8e:61:08:ca:45: - eb:0b:e7:54:bc:d9:20:69:6b:33:c6:e6:73:f9:40: - cc:76:34:3b:7a:c6:5f:26:29:56:a8:82:4d:2d:78: - 2d:53:07:f0:4e:f5:01:b6:48:54:e4:88:af:2d:75: - 39:bd:b6:71:60:2b:52:3b:44:d5:dc:8d:77:56:0b: - 9e:b2:7a:a3:c8:dd:48:50:64:93:5d:ce:c8:d0:08: - e4:4a:6b:f8:2b:02:6b:c4:af:11:44:c2:9b:a3:40: - 2b:2b:a2:f3:74:05:08:7c:0d:74:90:4c:5e:33:f7: - 1b:32:1f:08:03:aa:9c:93:67:8b:86:1c:54:f1:8a: - e9:a7:f7:67:7d:6e:4b:8e:23:01:7d:0e:ba:53:54: - 00:ce:3b:34:1d:5c:f4:10:bd:8f:c0:3f:85:1c:18: - 68:80:ee:78:8b:2f:50:47:f6:8a:8d:9d:24:50:ef: - c5:fc:f1:f7:1e:be:28:ce:98:8f:61:06:5c:c1:e9: - 94:f4:0c:f2:10:71:9e:23:6c:5b:8a:7d:cc:03:ba: - b9:2f + 00:d7:6c:bd:f2:d0:bc:4f:ca:5b:57:aa:d2:e9:d3: + c0:2f:9f:b7:2c:39:15:cf:21:66:4b:d8:c3:7d:f9: + 94:46:0e:0c:73:e4:da:64:cf:d2:5d:25:6c:dc:9a: + 0b:39:36:7f:fe:31:1e:1b:77:57:66:e1:ed:a4:ac: + 91:5c:99:4a:b9:9e:77:1a:a9:e0:93:f8:c8:00:02: + 20:d0:1f:35:61:66:60:4b:20:9c:11:e4:1e:e8:ee: + 1e:3a:ef:84:ae:f4:bd:a8:97:1d:72:37:0e:c1:95: + 86:f4:fc:35:d7:fe:f1:5a:05:45:bf:12:37:ca:cb: + 0a:4a:94:cf:1d:9b:61:45:75:2f:19:83:23:01:ff: + c7:74:d6:df:51:d0:98:30:bc:6d:36:14:e9:15:64: + a2:f2:68:6a:ab:4c:b7:e5:7e:9c:64:1f:36:96:07: + e9:d0:2c:29:fc:eb:b2:7a:d1:5c:65:6e:26:1f:11: + 2c:97:22:ab:04:13:cd:9b:62:52:3e:de:c9:ff:03: + 6f:97:f7:7e:9e:b7:99:9b:78:30:3c:1a:9c:e4:ac: + e3:a9:94:78:30:bd:b6:a4:9a:cf:57:69:7f:30:b9: + f4:d0:0f:26:c1:bc:8d:82:75:cb:1a:ad:9f:c2:14: + 10:fb:49:af:78:b5:1b:f6:d0:72:83:10:b0:52:d5: + e8:43 Exponent: 65537 (0x10001) X509v3 extensions: - X509v3 Subject Alternative Name: - DNS:localhost, DNS:example.com, IP Address:127.0.0.1 X509v3 Basic Constraints: CA:FALSE - X509v3 Key Usage: + X509v3 Subject Key Identifier: + A5:12:A1:7B:37:AA:D3:17:1F:BD:DD:CB:BC:D5:64:14:82:E5:3C:43 + X509v3 Authority Key Identifier: + keyid:39:6C:89:67:B2:33:EE:AF:67:61:74:DE:9A:C2:CF:DA:4F:6D:CD:40 + + X509v3 Key Usage: critical Digital Signature, Key Encipherment X509v3 Extended Key Usage: - TLS Web Server Authentication + TLS Web Server Authentication, TLS Web Client Authentication + X509v3 Subject Alternative Name: + DNS:localhost, DNS:example.com, IP Address:127.0.0.1 Signature Algorithm: sha256WithRSAEncryption - b0:14:09:45:a7:16:c2:fd:01:fa:a5:ca:dd:14:3b:5d:0d:80: - a2:d2:d6:ef:7c:bd:c1:49:98:05:35:52:6e:67:c7:9f:23:f2: - ad:b1:4c:0e:50:b2:c7:b0:0e:b6:44:42:5b:fe:21:29:71:6c: - 54:36:a9:a3:31:c6:1f:04:ee:a3:50:b5:fd:33:f9:d2:c4:58: - 11:e1:4d:c3:c3:b3:1f:db:42:95:11:ac:e6:f8:33:d5:46:97: - 2c:47:db:97:53:f1:7b:3f:14:bb:8a:b1:2f:f6:0a:11:82:8c: - ce:45:3a:25:91:a3:73:3a:95:90:89:d2:b2:cc:91:8c:d8:2a: - ff:3b:3d:40:ca:6d:d9:5d:05:7e:52:88:8b:b9:2b:a2:52:be: - 42:0a:09:a8:ba:2b:e3:5c:a6:f3:89:4a:50:40:e3:1a:a6:24: - 2f:b6:de:ce:7d:72:6c:fa:d3:2f:52:0f:16:00:9c:95:bf:54: - 20:1e:ab:99:25:66:61:f9:d5:1e:8b:0d:35:57:8b:d4:5e:16: - c8:3d:f3:7d:a4:2f:2e:55:1d:3f:e8:01:e7:eb:17:37:0c:0d: - ba:1e:9e:a7:33:9e:a9:90:12:36:4c:62:63:e5:02:f2:aa:33: - a6:99:bf:89:02:98:7d:cf:ff:cd:0f:f9:54:9a:9d:e7:83:dd: - 0b:8c:11:39 + 3f:31:6e:0f:da:fb:6e:01:80:61:f0:d9:90:ed:85:c2:12:f0: + 50:ae:5f:31:59:8f:36:37:f0:af:d8:02:4a:90:36:b5:c6:87: + 96:cf:5c:ba:e6:7a:f0:46:1a:eb:c2:0a:f2:50:11:67:d9:4a: + 55:98:04:41:9d:14:48:87:83:bc:9c:1b:34:4c:55:ff:75:d6: + ba:ce:6f:25:25:1f:b7:25:92:28:89:94:23:c4:62:23:6f:77: + 28:8c:59:4b:69:7c:5d:9b:98:35:b2:4d:5d:9e:d5:bc:3a:eb: + 8c:cf:81:11:5e:e3:8a:5d:c5:c8:b8:08:0a:99:75:01:e2:53: + 0c:b9:77:16:37:6b:01:dd:57:77:a5:b2:f6:a6:03:6d:73:85: + ca:54:88:4b:ae:7f:86:70:a1:c1:30:fe:b7:9a:a7:27:51:13: + f1:84:e3:2d:6c:54:ac:5f:6a:e0:e9:fc:89:59:43:e1:19:48: + 72:7a:94:5e:4f:5c:ff:ac:1c:86:97:f7:66:e6:e1:e4:8d:8b: + 7d:43:4e:ee:83:34:bc:7a:ef:c1:88:36:ad:e3:48:ae:8a:f7: + a8:64:63:90:f1:71:d4:06:93:c8:0b:24:aa:68:5f:17:6a:26: + 33:d3:48:e6:8d:e8:4d:21:fd:d7:db:68:11:11:ae:6b:a7:93: + 66:e2:ef:c9 -----BEGIN CERTIFICATE----- -MIIEATCCAumgAwIBAgIJAMXitkFg1TjpMA0GCSqGSIb3DQEBCwUAMIGUMQswCQYD +MIIEUDCCAzigAwIBAgIJAPtCONEvJga9MA0GCSqGSIb3DQEBCwUAMIGUMQswCQYD VQQGEwJVUzEQMA4GA1UECAwHTW9udGFuYTEQMA4GA1UEBwwHQm96ZW1hbjERMA8G A1UECgwIU2F3dG9vdGgxEzARBgNVBAsMCkNvbnN1bHRpbmcxGDAWBgNVBAMMD3d3 dy53b2xmc3NsLmNvbTEfMB0GCSqGSIb3DQEJARYQaW5mb0B3b2xmc3NsLmNvbTAe -Fw0yNjA2MTIxNzMwMTFaFw0zNjA2MDkxNzMwMTFaMIGQMQswCQYDVQQGEwJVUzEQ +Fw0yNjA2MTUxNjM2NDRaFw0zNjA2MTIxNjM2NDRaMIGQMQswCQYDVQQGEwJVUzEQ MA4GA1UECAwHTW9udGFuYTEQMA4GA1UEBwwHQm96ZW1hbjEQMA4GA1UECgwHd29s ZlNTTDEQMA4GA1UECwwHU3VwcG9ydDEYMBYGA1UEAwwPd3d3LndvbGZzc2wuY29t MR8wHQYJKoZIhvcNAQkBFhBpbmZvQHdvbGZzc2wuY29tMIIBIjANBgkqhkiG9w0B -AQEFAAOCAQ8AMIIBCgKCAQEAyiyM/Peu+8FL6cYaOFLnB/jsm2Nth54ytL0iy5yu -p2Rl00LFaklYe75cqNcKPZOoJ/2q6xZGjmEIykXrC+dUvNkgaWszxuZz+UDMdjQ7 -esZfJilWqIJNLXgtUwfwTvUBtkhU5IivLXU5vbZxYCtSO0TV3I13VguesnqjyN1I -UGSTXc7I0AjkSmv4KwJrxK8RRMKbo0ArK6LzdAUIfA10kExeM/cbMh8IA6qck2eL -hhxU8Yrpp/dnfW5LjiMBfQ66U1QAzjs0HVz0EL2PwD+FHBhogO54iy9QR/aKjZ0k -UO/F/PH3Hr4ozpiPYQZcwemU9AzyEHGeI2xbin3MA7q5LwIDAQABo1gwVjAnBgNV -HREEIDAegglsb2NhbGhvc3SCC2V4YW1wbGUuY29thwR/AAABMAkGA1UdEwQCMAAw -CwYDVR0PBAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA0GCSqGSIb3DQEBCwUA -A4IBAQCwFAlFpxbC/QH6pcrdFDtdDYCi0tbvfL3BSZgFNVJuZ8efI/KtsUwOULLH -sA62REJb/iEpcWxUNqmjMcYfBO6jULX9M/nSxFgR4U3Dw7Mf20KVEazm+DPVRpcs -R9uXU/F7PxS7irEv9goRgozORTolkaNzOpWQidKyzJGM2Cr/Oz1Aym3ZXQV+UoiL -uSuiUr5CCgmouivjXKbziUpQQOMapiQvtt7OfXJs+tMvUg8WAJyVv1QgHquZJWZh -+dUeiw01V4vUXhbIPfN9pC8uVR0/6AHn6xc3DA26Hp6nM56pkBI2TGJj5QLyqjOm -mb+JAph9z//ND/lUmp3ng90LjBE5 +AQEFAAOCAQ8AMIIBCgKCAQEA12y98tC8T8pbV6rS6dPAL5+3LDkVzyFmS9jDffmU +Rg4Mc+TaZM/SXSVs3JoLOTZ//jEeG3dXZuHtpKyRXJlKuZ53Gqngk/jIAAIg0B81 +YWZgSyCcEeQe6O4eOu+ErvS9qJcdcjcOwZWG9Pw11/7xWgVFvxI3yssKSpTPHZth +RXUvGYMjAf/HdNbfUdCYMLxtNhTpFWSi8mhqq0y35X6cZB82lgfp0Cwp/OuyetFc +ZW4mHxEslyKrBBPNm2JSPt7J/wNvl/d+nreZm3gwPBqc5KzjqZR4ML22pJrPV2l/ +MLn00A8mwbyNgnXLGq2fwhQQ+0mveLUb9tBygxCwUtXoQwIDAQABo4GmMIGjMAkG +A1UdEwQCMAAwHQYDVR0OBBYEFKUSoXs3qtMXH73dy7zVZBSC5TxDMB8GA1UdIwQY +MBaAFDlsiWeyM+6vZ2F03prCz9pPbc1AMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUE +FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwJwYDVR0RBCAwHoIJbG9jYWxob3N0ggtl +eGFtcGxlLmNvbYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAPzFuD9r7bgGAYfDZ +kO2FwhLwUK5fMVmPNjfwr9gCSpA2tcaHls9cuuZ68EYa68IK8lARZ9lKVZgEQZ0U +SIeDvJwbNExV/3XWus5vJSUftyWSKImUI8RiI293KIxZS2l8XZuYNbJNXZ7VvDrr +jM+BEV7jil3FyLgICpl1AeJTDLl3FjdrAd1Xd6Wy9qYDbXOFylSIS65/hnChwTD+ +t5qnJ1ET8YTjLWxUrF9q4On8iVlD4RlIcnqUXk9c/6wchpf3Zubh5I2LfUNO7oM0 +vHrvwYg2reNIror3qGRjkPFx1AaTyAskqmhfF2omM9NI5o3oTSH919toERGua6eT +ZuLvyQ== -----END CERTIFICATE----- -Certificate: - Data: - Version: 1 (0x0) - Serial Number: - cb:f5:0d:cd:48:75:df:5e - Signature Algorithm: sha256WithRSAEncryption - Issuer: C=US, ST=Montana, L=Bozeman, O=Sawtooth, OU=Consulting, CN=www.wolfssl.com/emailAddress=info@wolfssl.com - Validity - Not Before: Jun 12 17:30:11 2026 GMT - Not After : Jun 9 17:30:11 2036 GMT - Subject: C=US, ST=Montana, L=Bozeman, O=Sawtooth, OU=Consulting, CN=www.wolfssl.com/emailAddress=info@wolfssl.com - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - RSA Public-Key: (2048 bit) - Modulus: - 00:ce:6b:43:26:a4:36:e0:da:0b:ee:4b:84:5f:72: - df:fe:22:76:34:44:6c:c9:45:8c:8b:be:8b:f2:af: - 67:aa:74:25:48:77:e6:44:6c:80:69:9e:ae:d2:ec: - 5c:c3:e3:f2:51:4a:6c:c9:c2:c9:cb:5e:6d:74:d9: - 28:08:e9:6d:2e:6e:5b:7b:60:b8:17:cf:90:fc:dc: - 0b:fc:8f:e8:e9:4a:6f:e8:28:7d:50:d4:f3:a1:ea: - 29:62:92:15:82:f4:3b:74:88:6d:6f:ce:a5:63:bf: - 13:fc:34:03:62:ab:c7:24:6e:4e:62:69:88:6a:7f: - af:89:78:2c:64:88:0a:6e:59:b7:a1:43:43:ce:f3: - db:7a:97:af:f2:15:d1:e2:59:ea:65:8e:94:14:92: - 16:45:d6:d0:11:c1:5d:b9:04:00:96:6b:f3:cc:e6: - bb:ab:37:f7:d8:8d:29:a1:b0:d0:cc:dd:60:50:90: - 1d:f1:8d:b0:c9:01:6a:49:d2:b0:df:79:37:3c:b0: - c5:a0:4b:54:36:a0:b0:aa:c4:a1:09:66:6a:3a:07: - a6:42:d4:83:b1:9a:30:e2:8d:fd:41:01:86:2c:6a: - bf:41:5d:d6:37:20:34:9d:2b:be:de:0f:d7:d9:4e: - fe:e0:97:53:96:02:62:f3:87:55:de:47:bc:83:26: - dd:29 - Exponent: 65537 (0x10001) - Signature Algorithm: sha256WithRSAEncryption - 57:a9:91:4b:3c:fe:1e:b3:58:58:07:82:7f:a8:33:9b:e6:2f: - f5:e6:de:d0:72:00:1a:04:6f:15:cc:01:8b:10:1f:19:e3:d2: - 8e:d9:27:48:6c:2d:ef:c2:1e:a3:97:fe:3f:bd:cf:97:5a:1b: - 85:19:20:63:e6:2a:85:6d:08:cb:b8:38:c1:38:3f:c9:88:90: - e2:48:f4:c4:22:d2:3f:78:63:f0:a8:1b:26:08:2b:23:82:2c: - f0:e9:51:21:65:58:87:ad:73:03:41:95:67:ff:e9:b9:3d:93: - 19:49:ff:39:6a:75:c6:74:3b:e9:51:d0:07:28:14:30:f9:1a: - b2:23:63:b6:25:64:f0:3a:62:bf:84:40:65:ea:38:6c:f6:86: - 13:fb:94:98:ac:03:37:6e:e5:75:28:65:20:69:b2:f9:9a:2c: - e6:af:ad:74:78:d8:5f:f4:d2:6c:4d:32:6c:3a:ab:65:6b:d7: - 3c:f9:fb:6a:18:fb:c0:88:e4:40:0b:38:cd:d4:5a:1c:b0:bb: - 18:3a:6a:18:20:0f:27:0e:ea:38:13:50:2c:f4:3a:00:dc:e9: - 1a:d5:52:21:cb:3c:7c:e1:25:e8:d5:e1:b4:2e:79:2c:45:8a: - cb:63:90:60:57:96:a0:17:6f:d8:cf:05:76:b1:51:12:86:a3: - 58:36:00:5c -----BEGIN CERTIFICATE----- -MIIDpjCCAo4CCQDL9Q3NSHXfXjANBgkqhkiG9w0BAQsFADCBlDELMAkGA1UEBhMC -VVMxEDAOBgNVBAgMB01vbnRhbmExEDAOBgNVBAcMB0JvemVtYW4xETAPBgNVBAoM -CFNhd3Rvb3RoMRMwEQYDVQQLDApDb25zdWx0aW5nMRgwFgYDVQQDDA93d3cud29s -ZnNzbC5jb20xHzAdBgkqhkiG9w0BCQEWEGluZm9Ad29sZnNzbC5jb20wHhcNMjYw -NjEyMTczMDExWhcNMzYwNjA5MTczMDExWjCBlDELMAkGA1UEBhMCVVMxEDAOBgNV -BAgMB01vbnRhbmExEDAOBgNVBAcMB0JvemVtYW4xETAPBgNVBAoMCFNhd3Rvb3Ro -MRMwEQYDVQQLDApDb25zdWx0aW5nMRgwFgYDVQQDDA93d3cud29sZnNzbC5jb20x -HzAdBgkqhkiG9w0BCQEWEGluZm9Ad29sZnNzbC5jb20wggEiMA0GCSqGSIb3DQEB -AQUAA4IBDwAwggEKAoIBAQDOa0MmpDbg2gvuS4Rfct/+InY0RGzJRYyLvovyr2eq -dCVId+ZEbIBpnq7S7FzD4/JRSmzJwsnLXm102SgI6W0ublt7YLgXz5D83Av8j+jp -Sm/oKH1Q1POh6ilikhWC9Dt0iG1vzqVjvxP8NANiq8ckbk5iaYhqf6+JeCxkiApu -WbehQ0PO89t6l6/yFdHiWepljpQUkhZF1tARwV25BACWa/PM5rurN/fYjSmhsNDM -3WBQkB3xjbDJAWpJ0rDfeTc8sMWgS1Q2oLCqxKEJZmo6B6ZC1IOxmjDijf1BAYYs -ar9BXdY3IDSdK77eD9fZTv7gl1OWAmLzh1XeR7yDJt0pAgMBAAEwDQYJKoZIhvcN -AQELBQADggEBAFepkUs8/h6zWFgHgn+oM5vmL/Xm3tByABoEbxXMAYsQHxnj0o7Z -J0hsLe/CHqOX/j+9z5daG4UZIGPmKoVtCMu4OME4P8mIkOJI9MQi0j94Y/CoGyYI -KyOCLPDpUSFlWIetcwNBlWf/6bk9kxlJ/zlqdcZ0O+lR0AcoFDD5GrIjY7YlZPA6 -Yr+EQGXqOGz2hhP7lJisAzdu5XUoZSBpsvmaLOavrXR42F/00mxNMmw6q2Vr1zz5 -+2oY+8CI5EALOM3UWhywuxg6ahggDycO6jgTUCz0OgDc6RrVUiHLPHzhJejV4bQu -eSxFistjkGBXlqAXb9jPBXaxURKGo1g2AFw= +MIIE3TCCA8WgAwIBAgIJALio751TFjirMA0GCSqGSIb3DQEBCwUAMIGUMQswCQYD +VQQGEwJVUzEQMA4GA1UECAwHTW9udGFuYTEQMA4GA1UEBwwHQm96ZW1hbjERMA8G +A1UECgwIU2F3dG9vdGgxEzARBgNVBAsMCkNvbnN1bHRpbmcxGDAWBgNVBAMMD3d3 +dy53b2xmc3NsLmNvbTEfMB0GCSqGSIb3DQEJARYQaW5mb0B3b2xmc3NsLmNvbTAe +Fw0yNjA2MTUxNjM2NDRaFw0zNjA2MTIxNjM2NDRaMIGUMQswCQYDVQQGEwJVUzEQ +MA4GA1UECAwHTW9udGFuYTEQMA4GA1UEBwwHQm96ZW1hbjERMA8GA1UECgwIU2F3 +dG9vdGgxEzARBgNVBAsMCkNvbnN1bHRpbmcxGDAWBgNVBAMMD3d3dy53b2xmc3Ns +LmNvbTEfMB0GCSqGSIb3DQEJARYQaW5mb0B3b2xmc3NsLmNvbTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAL2vkxFd0Pc4YBF/Sle8tpop9H7IjaocfMOW +k5s4GuUEwF26zWXTecVkC53oNZQfDtiWUZlWdobvY4XqwYSbt4MaAGtsG8N+HM5F +KH6BZM1Mw8yUUMlcVMiPe32/VQp+M0UZg8vjU5xVSgOuQv6Qcww2ieSCvcSur9RW +cM73ASmWAv9oclJXIT1LUja7mYAClTAW6dQvb4lj59vZrX8CLYqP9tMyaThnicYN +TfHi4MnvEJaurrJGPIk5eTeFXw9Sg/XMBRdnYFJkHJmWTW/5F8a5pbxuj6ZlFsDb +DntixcrMSYZWOEu3qv7qRTYJIF7pUnpSoEvNGYzvwDssmLt1u60CAwEAAaOCAS4w +ggEqMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFDlsiWeyM+6vZ2F03prCz9pP +bc1AMIHJBgNVHSMEgcEwgb6AFDlsiWeyM+6vZ2F03prCz9pPbc1AoYGapIGXMIGU +MQswCQYDVQQGEwJVUzEQMA4GA1UECAwHTW9udGFuYTEQMA4GA1UEBwwHQm96ZW1h +bjERMA8GA1UECgwIU2F3dG9vdGgxEzARBgNVBAsMCkNvbnN1bHRpbmcxGDAWBgNV +BAMMD3d3dy53b2xmc3NsLmNvbTEfMB0GCSqGSIb3DQEJARYQaW5mb0B3b2xmc3Ns +LmNvbYIJALio751TFjirMA4GA1UdDwEB/wQEAwIBBjAcBgNVHREEFTATggtleGFt +cGxlLmNvbYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAS1IveJ023jHZ+RclOvKz +1X35NocNZFk8Ic1dWLE4hjf98vl310X5qtuBDaliOBRs0yYvWe2KQg26VeKTJbO5 +wjZhTN7iU/Om1LrUyrFAhmtgVY6JUIaSymJiNqQL4u/KMXbN3Z9NIKZLKJY2+tWx +gL6e34Evz1KYC2P53vrqQb9eQwteVRWlcgerScZU+8dIuqQx/vrhx90Mwuh4EASm +vf6IABha19SkWPjFlwMW3JzRutGUYYezXkUxQAkhcc7plPxTKXh4psahvj5Vb458 +zY8/GtjTrCRXNFEqfdqmKLvjdQ2P4+i1Mk7bDsnjYlz9Qy0IfyVzg06m/XajUdkW +LA== -----END CERTIFICATE----- diff --git a/scripts/broker_test/server-key.pem b/scripts/broker_test/server-key.pem index ede188463..2fb62f35f 100644 --- a/scripts/broker_test/server-key.pem +++ b/scripts/broker_test/server-key.pem @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAyiyM/Peu+8FL6cYaOFLnB/jsm2Nth54ytL0iy5yup2Rl00LF -aklYe75cqNcKPZOoJ/2q6xZGjmEIykXrC+dUvNkgaWszxuZz+UDMdjQ7esZfJilW -qIJNLXgtUwfwTvUBtkhU5IivLXU5vbZxYCtSO0TV3I13VguesnqjyN1IUGSTXc7I -0AjkSmv4KwJrxK8RRMKbo0ArK6LzdAUIfA10kExeM/cbMh8IA6qck2eLhhxU8Yrp -p/dnfW5LjiMBfQ66U1QAzjs0HVz0EL2PwD+FHBhogO54iy9QR/aKjZ0kUO/F/PH3 -Hr4ozpiPYQZcwemU9AzyEHGeI2xbin3MA7q5LwIDAQABAoIBAQDFmvXS62QkvbGt -NOu70Yvuxua8mlocDAwTjCnOSb6L7h14d/LtB/NsP4vhmw1vUjsxm0bLsGIWF9G3 -os8yO1EfpDmB0D4zUlxYa3Vss3DPd8TYT99bpMA6iRQD6+Z9xgt+VwRiuxY9oC5n -t0LpdG5Tb9x4Te0uNP1QBX7AfUiJCYd4ePpbe7WBgANDXdUGbo8V6sEVie939d43 -Cijk4Q1r3bg2uOfK/nEvCH2aFfgNTO+1+0t8gXFvqM+6DobACZXEPQfV04Gj1azI -VOZ3fq9l7rawJja+LWcrYGTgUN7NamcnZIj1tCNWGmjcXoIlMcxUyRwsjUm6AP1i -5ZmtU6sBAoGBAPBuKG/NERIaYEvJgsIAVvx81Ujlg2lVxPyqxltydCZzIRO8kpGy -AaavRovw0MmLMgD9V7PF2mipjV6BcQac0rebkyV9yYLV/yKIFeVaP7QB2BPjEnbp -SgQIt9fm11+pGMTDtqoCYCFAAN6P7xaX3Qz/OLcMwRhXzG/kmBArGShfAoGBANdE -LpnloB4qE6W3mE/gZrPKu7Tk5Bg/i5RTG1CslZ6BAVPI0izUdsoMUbn7Co1G9PRg -u6lFZg9J8Tvx8Q68fQS9oAFRS0OmOu8+aL/dtsPhMFN4XlxZ+Vra6x85wcZz8cdx -ZEm2e+tRFl40t+AgcQuG4zCJdGPgPFyvglCBImExAoGAYi35oT3yPJw8unX9SU9u -Ngib5/qhIQB/QlZSTcF9IL5ewXp9t7Ui63gjrL2X5NVMhA7wI18mAxtJuU/OYc7k -VUnYWrT09tKALw+3MUMbRFyEagqN3bUCHoeY2zdOt6eLj74D94Sk0K8cK8ZG8cjt -4YLPHCC/MTuZJhAI8IFI8q0CgYAft3QGSMbmqtxqNjrCyhVXuC8f3/mbeQFfwT7t -DACfcfJ4HcaZxFQcQORpuos7dZDx0K7Vqdv3tLVOV79kpHDnGelRSGEGfp+AUHmu -i0Q6aBtusPV2Net/b3HlD+V1D/A3qoVUNwbbDP92sd3FsAH36M/gfuAfNxKttU5F -/kDKgQKBgD4WHVDW9Ey0sZoW3srKYeDyN/C0AV8eXonFvdG9hqLqs67zk6/gg3YK -U0eCHiUGiI4JKzVPKtVLfEY65/Ig8HJ96O3FzZ37xVXqForRBY2rPZgreNLhDECL -K4hiAG1YPSuvbBYbMQCwyLbDPvobbFb3MhDc1TAkFDOcnc4PTEDd +MIIEowIBAAKCAQEA12y98tC8T8pbV6rS6dPAL5+3LDkVzyFmS9jDffmURg4Mc+Ta +ZM/SXSVs3JoLOTZ//jEeG3dXZuHtpKyRXJlKuZ53Gqngk/jIAAIg0B81YWZgSyCc +EeQe6O4eOu+ErvS9qJcdcjcOwZWG9Pw11/7xWgVFvxI3yssKSpTPHZthRXUvGYMj +Af/HdNbfUdCYMLxtNhTpFWSi8mhqq0y35X6cZB82lgfp0Cwp/OuyetFcZW4mHxEs +lyKrBBPNm2JSPt7J/wNvl/d+nreZm3gwPBqc5KzjqZR4ML22pJrPV2l/MLn00A8m +wbyNgnXLGq2fwhQQ+0mveLUb9tBygxCwUtXoQwIDAQABAoIBAB5OihqTCyseiOM4 +gRusUqlgiuCJ12ugg0fAYyBh8F46s73KQH9WTX4VOc9/THzsEe2s81Nh4sXymwBD +1t90IXMjvBXgOFwY7+owYnVlLplZUcq/97T8puyWM2KPN9d1Twlc2SOsY0MQyj/F +C74re8DyZPGQmpDJJ9V+QfUkWvbuV03r51QSXWW1l5LdAlnf0X8nZG/btKD3QLEv +qP7s0f7LpCGmIi49fdpmlKC5PEYJKShd0d3d04U47LVixhHO4Y+YCpznA6B3CcRR +0ofAiHHJV9bqp6n8GSV/8z5zHukQQocNJgHiGg3hv5SVnqtYLAkGKgtkCMLzMQoT +gB+r4yECgYEA+sJbVtG2RuI9FhtK/g8S1I3CU6AfEoJVPc+YGv9USvAopKr2Yb6e +NcTWjtkVyq1WBpTPjsrgdlHHqdSvGUg+nGgEE519+qjNS7oVbo/M5zScpdij1d7h +lkMJMnI5j3cvWWVo6II+k4SPTwaebFwNrgQt35+/OApMxTNX3OpTZh8CgYEA2+1V +ocNmAS5zNict1CMoObot1VcPBNAlyaq3jQ5uQ1vRK+06Ev912coB11nSwPDrKkVQ +jEI3SH35dJDZVQvP8+EknXq1B86GZY39KnkEvGXvUlsIcOEGzHv4qJnul/relP57 +rToMBxkBUkEBq/xvsQJgQKTs49dNeoU9Q0WGUV0CgYEAvXGoX+b/tn2leNYVyerJ +ZxvR/Cu0Td95VsFHQN41aIgXrJAco6vHCwgysKkA9aYOn9o9FLvg6ILQPVYZExip +dLCAo/EQBBcTQmrLVkP9oY716bXJ0QIZm9P5VstFAUYh13/tyfrcG8bCHgn6FhNV +omo13gRqCoR9i15GuvetGjECgYBbIRjekqztyGWNBucChB7i7LaZNB3RDL7btZWh +KV2fI6ik3wO1Y41d1Uq2BU5DDJElTtt1guqNa0W2e17S0rY0hBI7/uCMf5NI+XDx +7Ht95W8pXPc+br/2c1gx5Lbs2tLoQhrVSLw7JK1be/xJW6ycWDOyFaTjNA+yuQsN +mP1zMQKBgBm6ile0Rxa34x0NwU0bpDslVFo60YBap8To2bf/Bf3+hLcDw4MyPrkI +FeELGPHBNIhB6WIEQrToCqjpZct5Ke5g6RLczBneqdkij6xmYyBSyU+5ZUV17CVf +sZb3Pvh5CKDu5/sKUXboWSHuPOwuxgpE24MOmeFahqizsbvQnxs+ -----END RSA PRIVATE KEY----- From 408d499e92e9490ac1bd012d05cf288e668500aa Mon Sep 17 00:00:00 2001 From: aidan garske Date: Mon, 15 Jun 2026 14:48:51 -0700 Subject: [PATCH 34/40] Bound reason-code read by received buffer length in v5 PublishResp, Disconnect, and Auth decoders --- src/mqtt_packet.c | 15 ++++++++++++ tests/test_mqtt_packet.c | 50 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index 69b885b9d..a89b4d35d 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -2270,6 +2270,11 @@ int MqttDecode_PublishResp(byte* rx_buf, int rx_buf_len, byte type, publish_resp->props = NULL; if (publish_resp->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { if (remain_len > MQTT_DATA_LEN_SIZE) { + /* remain_len is attacker-controlled; confirm the reason-code + * byte was received before reading past the buffer */ + if (rx_buf_len < (rx_payload - rx_buf) + 1) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + } /* Decode the Reason Code */ publish_resp->reason_code = *rx_payload++; } @@ -3307,6 +3312,11 @@ int MqttDecode_Disconnect(byte *rx_buf, int rx_buf_len, MqttDisconnect *disc) disc->props = NULL; if (remain_len > 0) { + /* remain_len is attacker-controlled; confirm the reason-code byte + * was received before reading past the buffer */ + if (rx_buf_len < (rx_payload - rx_buf) + 1) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + } /* Decode variable header */ disc->reason_code = *rx_payload++; @@ -3441,6 +3451,11 @@ int MqttDecode_Auth(byte *rx_buf, int rx_buf_len, MqttAuth *auth) auth->props = NULL; if (remain_len > 0) { + /* remain_len is attacker-controlled; confirm the reason-code byte + * was received before reading past the buffer */ + if (rx_buf_len < (rx_payload - rx_buf) + 1) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + } /* Decode variable header */ auth->reason_code = *rx_payload++; if ((auth->reason_code == MQTT_REASON_SUCCESS) || diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index 0963fe9a5..e0f7099c0 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -3990,6 +3990,22 @@ TEST(decode_puback_v5_with_reason_code_accepted) ASSERT_EQ(0, resp.reason_code); MqttProps_Free(resp.props); } + +/* Remaining Length advertises a Reason Code but the supplied buffer stops + * right after the Packet Identifier. The decoder must reject the short frame + * instead of reading the reason-code byte out of bounds. */ +TEST(decode_puback_v5_reason_code_past_buf_rejected) +{ + byte buf[4] = { 0x40, 0x03, 0x00, 0x05 }; + MqttPublishResp resp; + int rc; + + XMEMSET(&resp, 0, sizeof(resp)); + resp.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_PublishResp(buf, (int)sizeof(buf), + MQTT_PACKET_TYPE_PUBLISH_ACK, &resp); + ASSERT_EQ(MQTT_CODE_ERROR_OUT_OF_BUFFER, rc); +} #endif /* WOLFMQTT_V5 */ /* ============================================================================ @@ -4275,6 +4291,20 @@ TEST(decode_disconnect_v5_with_reason_code_accepted) ASSERT_EQ(0, disc.reason_code); MqttProps_Free(disc.props); } + +/* Remaining Length advertises a Reason Code but the supplied buffer is only + * the fixed header. The decoder must reject the short frame instead of + * reading the reason-code byte out of bounds. */ +TEST(decode_disconnect_v5_reason_code_past_buf_rejected) +{ + byte buf[2] = { 0xE0, 0x01 }; + MqttDisconnect disc; + int rc; + + XMEMSET(&disc, 0, sizeof(disc)); + rc = MqttDecode_Disconnect(buf, (int)sizeof(buf), &disc); + ASSERT_EQ(MQTT_CODE_ERROR_OUT_OF_BUFFER, rc); +} #endif /* WOLFMQTT_V5 */ /* ============================================================================ @@ -4726,6 +4756,23 @@ TEST(auth_v5_invalid_reason_code_rejected) dec_len = MqttDecode_Auth(buf, 4, &dec); ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, dec_len); } + +/* Remaining Length advertises a Reason Code but the supplied buffer is only + * the fixed header. The decoder must reject the short frame instead of + * reading the reason-code byte out of bounds. */ +TEST(decode_auth_v5_reason_code_past_buf_rejected) +{ + byte buf[2]; + MqttAuth dec; + int dec_len; + + buf[0] = (byte)(MQTT_PACKET_TYPE_AUTH << 4); + buf[1] = 0x01; /* Remaining Length claims a reason-code byte */ + + XMEMSET(&dec, 0, sizeof(dec)); + dec_len = MqttDecode_Auth(buf, (int)sizeof(buf), &dec); + ASSERT_EQ(MQTT_CODE_ERROR_OUT_OF_BUFFER, dec_len); +} #endif /* WOLFMQTT_V5 */ /* ============================================================================ @@ -5002,6 +5049,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_puback_null_resp_extra_payload_rejected); #ifdef WOLFMQTT_V5 RUN_TEST(decode_puback_v5_with_reason_code_accepted); + RUN_TEST(decode_puback_v5_reason_code_past_buf_rejected); #endif /* MqttEncode_PublishResp fixed-header QoS bits */ @@ -5032,6 +5080,7 @@ void run_mqtt_packet_tests(void) #ifdef WOLFMQTT_V5 RUN_TEST(decode_disconnect_v5_invalid_fixed_header_flags_rejected); RUN_TEST(decode_disconnect_v5_with_reason_code_accepted); + RUN_TEST(decode_disconnect_v5_reason_code_past_buf_rejected); #endif /* Fixed-header reserved-flag validation [MQTT-2.2.2-2] */ @@ -5060,6 +5109,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(auth_v5_reauth_decodes_without_error); RUN_TEST(auth_v5_success_remaining_length_zero); RUN_TEST(auth_v5_invalid_reason_code_rejected); + RUN_TEST(decode_auth_v5_reason_code_past_buf_rejected); #endif TEST_SUITE_END(); From 4ff31fbaee6d3178db761e3afd03791ee73fd047 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 16 Jun 2026 08:44:16 -0700 Subject: [PATCH 35/40] Address Skoll review: lock rx_buf scrub, guard fan-out on socket liveness, and harden per-client sub cap default --- src/mqtt_broker.c | 14 ++++++++++++-- src/mqtt_client.c | 16 +++++++++++----- wolfmqtt/mqtt_broker.h | 12 ++++++++---- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index c1439e11d..5c085defc 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -4935,6 +4935,14 @@ static int BrokerHandle_Subscribe(BrokerClient* bc, int rx_len, } if (sub_rc != MQTT_CODE_SUCCESS) { granted_qos = (MqttQoS)fail_code; + #ifdef WOLFMQTT_V5 + /* A capacity rejection (per-client cap or full table) maps to + * the v5 Quota Exceeded reason so the client sees why. */ + if (bc->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5 && + sub_rc == MQTT_CODE_ERROR_MEMORY) { + granted_qos = (MqttQoS)MQTT_REASON_QUOTA_EXCEEDED; + } + #endif } #ifdef WOLFMQTT_BROKER_RETAINED else { @@ -5270,6 +5278,7 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, #endif if (sub->client != NULL && sub->client->protocol_level != 0 && + sub->client->sock != BROKER_SOCKET_INVALID && BROKER_STR_VALID(sub->filter) && BrokerTopicMatch(sub->filter, topic)) { MqttQoS eff_qos; @@ -5314,8 +5323,9 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, /* Static fan-out has no per-subscriber resume queue, so * a partial write leaves this subscriber's stream * desynced and unrecoverable. Tear down its socket; the - * main loop reaps it on the next read error. Removal is - * deferred so next_sub stays valid this iteration. */ + * main loop reaps it on the next read error. The match + * guard above then skips this client's other matching + * subscriptions once its socket is invalidated. */ if (wr != sub_rc && sub->client->sock != BROKER_SOCKET_INVALID) { broker->net.close(broker->net.ctx, diff --git a/src/mqtt_client.c b/src/mqtt_client.c index 8559d87f4..7c0ce4ddb 100644 --- a/src/mqtt_client.c +++ b/src/mqtt_client.c @@ -2916,12 +2916,18 @@ int MqttClient_Auth(MqttClient *client, MqttAuth* auth) /* Scrub the decoded AUTH response from rx_buf. Its properties (e.g. the * MQTT_PROP_AUTH_DATA SASL blob) point into rx_buf and would otherwise - * linger until the next read overwrites them. This is safe to do here: - * MqttClient_WaitType above has already run MqttClient_DecodePacket, which - * delivered auth->props to the property callback and then freed them and - * set auth->props = NULL. So the bytes are consumed before this scrub and - * the caller has no live pointer into rx_buf. */ + * linger until the next read overwrites them. MqttClient_WaitType above + * already delivered and freed auth->props, so the bytes are consumed + * before this scrub and the caller has no live pointer into rx_buf. */ +#ifdef WOLFMQTT_MULTITHREAD + /* Hold lockRecv so the scrub cannot race a concurrent read into rx_buf. */ + if (wm_SemLock(&client->lockRecv) == 0) { + CLIENT_FORCE_ZERO(client->rx_buf, client->rx_buf_len); + wm_SemUnlock(&client->lockRecv); + } +#else CLIENT_FORCE_ZERO(client->rx_buf, client->rx_buf_len); +#endif /* reset state */ auth->stat.write = MQTT_MSG_BEGIN; diff --git a/wolfmqtt/mqtt_broker.h b/wolfmqtt/mqtt_broker.h index 2242b6cdd..a076b1509 100644 --- a/wolfmqtt/mqtt_broker.h +++ b/wolfmqtt/mqtt_broker.h @@ -96,12 +96,16 @@ /* Per-client subscription cap so one client cannot occupy the whole shared * subscription table and deny other clients. */ #ifndef BROKER_MAX_SUBS_PER_CLIENT - /* At least 1 so a config where BROKER_MAX_CLIENTS > BROKER_MAX_SUBS does - * not collapse the cap to 0 and reject every subscription. */ - #if (BROKER_MAX_SUBS / BROKER_MAX_CLIENTS) > 0 + /* Fair share of the shared table, but floored at 8 (and never above the + * table size) so the default does not silently reject a client that + * subscribes to a normal handful of topics. Override to tighten the + * anti-DoS cap or to raise it for subscription-heavy clients. */ + #if (BROKER_MAX_SUBS / BROKER_MAX_CLIENTS) >= 8 #define BROKER_MAX_SUBS_PER_CLIENT (BROKER_MAX_SUBS / BROKER_MAX_CLIENTS) + #elif BROKER_MAX_SUBS < 8 + #define BROKER_MAX_SUBS_PER_CLIENT BROKER_MAX_SUBS #else - #define BROKER_MAX_SUBS_PER_CLIENT 1 + #define BROKER_MAX_SUBS_PER_CLIENT 8 #endif #endif #ifndef BROKER_MAX_CLIENT_ID_LEN From 9f5f5e4b1a16136b30532c38ae10bda2abbfa67d Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 16 Jun 2026 09:01:43 -0700 Subject: [PATCH 36/40] Bound v5 UNSUBACK properties by packet end and avoid narrowing firmware topic compare --- examples/firmware/fwclient.c | 10 ++++++---- src/mqtt_packet.c | 20 +++++++++++++------- tests/test_mqtt_packet.c | 23 +++++++++++++++++++++++ 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/examples/firmware/fwclient.c b/examples/firmware/fwclient.c index d1b889961..3b9ad37e0 100644 --- a/examples/firmware/fwclient.c +++ b/examples/firmware/fwclient.c @@ -187,12 +187,14 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, byte msg_new, byte msg_done) { MQTTCtx* mqttCtx = (MQTTCtx*)client->ctx; + size_t topic_len = XSTRLEN(mqttCtx->topic_name); - /* Verify this message is for the firmware topic */ + /* Verify this message is for the firmware topic. Compare lengths without + * narrowing to word16 so an overlong configured topic cannot alias a + * 16-bit received length and over-read the length-delimited topic_name. */ if (msg_new && - msg->topic_name_len == (word16)XSTRLEN(mqttCtx->topic_name) && - XSTRNCMP(msg->topic_name, mqttCtx->topic_name, - XSTRLEN(mqttCtx->topic_name)) == 0 && + (size_t)msg->topic_name_len == topic_len && + XMEMCMP(msg->topic_name, mqttCtx->topic_name, topic_len) == 0 && !mFwBuf) { /* Allocate buffer for entire message */ diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index a89b4d35d..fc541e495 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -2977,26 +2977,29 @@ int MqttDecode_UnsubscribeAck(byte *rx_buf, int rx_buf_len, #ifdef WOLFMQTT_V5 unsubscribe_ack->props = NULL; if (unsubscribe_ack->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { + /* Bound properties by the packet end, not rx_buf_len: a caller + * buffer may hold bytes from the next packet, and consuming them + * here would push rx_payload past this packet and underflow + * reason_code_count to a huge word16. */ + byte* packet_end = &rx_buf[header_len + remain_len]; + if (remain_len > MQTT_DATA_LEN_SIZE) { word32 props_len = 0; int props_tmp; /* Decode Length of Properties */ - if (rx_buf_len < (rx_payload - rx_buf)) { - return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); - } props_tmp = MqttDecode_Vbi(rx_payload, &props_len, - (word32)(rx_buf_len - (rx_payload - rx_buf))); + (word32)(packet_end - rx_payload)); if (props_tmp < 0) return props_tmp; - if (props_len <= (word32)(rx_buf_len - (rx_payload - rx_buf))) { + if (props_len <= (word32)(packet_end - (rx_payload + props_tmp))) { rx_payload += props_tmp; if (props_len > 0) { /* Decode the Properties */ props_tmp = MqttDecode_Props(MQTT_PACKET_TYPE_UNSUBSCRIBE_ACK, &unsubscribe_ack->props, rx_payload, - (word32)(rx_buf_len - (rx_payload - rx_buf)), + (word32)(packet_end - rx_payload), props_len); if (props_tmp < 0) return props_tmp; @@ -3009,9 +3012,12 @@ int MqttDecode_UnsubscribeAck(byte *rx_buf, int rx_buf_len, } /* Reason codes are stored in the payload, one per topic filter */ + if (rx_payload > packet_end) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + } unsubscribe_ack->reason_codes = rx_payload; unsubscribe_ack->reason_code_count = - (word16)((rx_buf + header_len + remain_len) - rx_payload); + (word16)(packet_end - rx_payload); } #endif } diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index e0f7099c0..18245ccf7 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -4177,6 +4177,28 @@ TEST(decode_unsuback_v5_reason_codes) ASSERT_EQ(0x00, ack.reason_codes[0]); ASSERT_EQ(0x87, ack.reason_codes[1]); } + +/* A v5 UNSUBACK whose property length runs past its own Remaining Length must + * be rejected even when the caller buffer holds trailing bytes from the next + * packet: consuming them would push past the packet and underflow + * reason_code_count. props_len=3 but only one packet byte follows it. */ +TEST(decode_unsuback_v5_props_past_remain_len_rejected) +{ + byte buf[] = { + 0xB0, 0x04, /* UNSUBACK, remain_len = 4 */ + 0x00, 0x01, /* packet_id */ + 0x03, /* props_len = 3 (overruns packet) */ + 0x1F, /* last byte inside Remaining Length */ + 0x00, 0x00 /* trailing bytes from next packet */ + }; + MqttUnsubscribeAck ack; + int rc; + + XMEMSET(&ack, 0, sizeof(ack)); + ack.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_UnsubscribeAck(buf, (int)sizeof(buf), &ack); + ASSERT_EQ(MQTT_CODE_ERROR_OUT_OF_BUFFER, rc); +} #endif /* WOLFMQTT_V5 */ /* ============================================================================ @@ -5065,6 +5087,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_unsuback_truncated_remain_len_rejected); #ifdef WOLFMQTT_V5 RUN_TEST(decode_unsuback_v5_reason_codes); + RUN_TEST(decode_unsuback_v5_props_past_remain_len_rejected); #endif /* MqttDecode_Ping (PINGRESP) length validation */ From 62f7fb687b8ea5145201c65f53bdfbc0703c803f Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 16 Jun 2026 09:21:29 -0700 Subject: [PATCH 37/40] Fix WebSocket fan-out: use connected flag for liveness and reap deferred pending_remove to avoid double Will --- src/mqtt_broker.c | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 5c085defc..a61da5b54 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -5278,7 +5278,7 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, #endif if (sub->client != NULL && sub->client->protocol_level != 0 && - sub->client->sock != BROKER_SOCKET_INVALID && + sub->client->connected && BROKER_STR_VALID(sub->filter) && BrokerTopicMatch(sub->filter, topic)) { MqttQoS eff_qos; @@ -5322,10 +5322,10 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, sub->client->tx_buf, sub_rc); /* Static fan-out has no per-subscriber resume queue, so * a partial write leaves this subscriber's stream - * desynced and unrecoverable. Tear down its socket; the - * main loop reaps it on the next read error. The match - * guard above then skips this client's other matching - * subscriptions once its socket is invalidated. */ + * desynced and unrecoverable. Tear down its socket and + * clear connected; the main loop reaps it on the next + * read error, and the match guard above then skips this + * client's other matching subscriptions. */ if (wr != sub_rc && sub->client->sock != BROKER_SOCKET_INVALID) { broker->net.close(broker->net.ctx, @@ -5572,6 +5572,19 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) int rc; int activity = 0; +#ifdef ENABLE_MQTT_WEBSOCKET + /* A peer-initiated WS close during another client's fan-out write defers + * this client's removal via pending_remove, but no end-of-dispatch + * consumer runs on its own stack. Reap it here before any read so it is + * not re-closed by BrokerClient_AbnormalClose, which would publish its + * Will a second time. Will and subscriptions were handled at close. */ + if (bc->ws_ctx != NULL && + ((BrokerWsCtx*)bc->ws_ctx)->pending_remove) { + BrokerClient_Remove(broker, bc); + return 0; + } +#endif + #ifdef ENABLE_MQTT_TLS /* Complete TLS handshake before processing MQTT packets */ if (!bc->tls_handshake_done) { From 10f6c9dfaf0ec59a6fd648c5584d9fbf2423300c Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 16 Jun 2026 09:44:53 -0700 Subject: [PATCH 38/40] Reap expired retained messages before enforcing the retained cap on store and restore --- src/mqtt_broker.c | 58 ++++++++++++++++++++++++++++++++ src/mqtt_broker_persist.c | 9 +++++ tests/test_broker_connect.c | 66 +++++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index a61da5b54..f77a3481f 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -3148,6 +3148,60 @@ static int BrokerSubs_ReassociateClient(MqttBroker* broker, /* Retained message management */ /* -------------------------------------------------------------------------- */ #ifdef WOLFMQTT_BROKER_RETAINED +/* Free retained messages whose v5 Message Expiry Interval has elapsed so they + * stop occupying a slot / the retained_count cap. Delivery also reaps expired + * entries, but a publisher can hit the cap with only-expired entries before any + * subscription triggers that path. */ +static void BrokerRetained_ReapExpired(MqttBroker* broker, + WOLFMQTT_BROKER_TIME_T now) +{ +#ifdef WOLFMQTT_STATIC_MEMORY + int i; + for (i = 0; i < BROKER_MAX_RETAINED; i++) { + BrokerRetainedMsg* rm = &broker->retained[i]; + if (rm->in_use && rm->expiry_sec > 0 && + (now - rm->store_time) >= rm->expiry_sec) { + WBLOG_DBG(broker, "broker: retained expired topic=%s", + BrokerLog_Sanitize(rm->topic)); + XMEMSET(rm, 0, sizeof(BrokerRetainedMsg)); + } + } +#else + BrokerRetainedMsg* cur; + BrokerRetainedMsg* prev = NULL; + + /* A delivery loop may hold node pointers; defer to its post-loop reap. */ + if (broker->retained_delivering > 0) { + return; + } + cur = broker->retained; + while (cur != NULL) { + BrokerRetainedMsg* next = cur->next; + if (cur->expiry_sec > 0 && + (now - cur->store_time) >= cur->expiry_sec) { + WBLOG_DBG(broker, "broker: retained expired topic=%s", + BrokerLog_Sanitize(cur->topic)); + if (prev != NULL) { + prev->next = next; + } + else { + broker->retained = next; + } + if (cur->topic != NULL) WOLFMQTT_FREE(cur->topic); + if (cur->payload != NULL) WOLFMQTT_FREE(cur->payload); + WOLFMQTT_FREE(cur); + if (broker->retained_count > 0) { + broker->retained_count--; + } + } + else { + prev = cur; + } + cur = next; + } +#endif +} + static int BrokerRetained_Store(MqttBroker* broker, const char* topic, const byte* payload, word32 payload_len, MqttQoS qos, word32 expiry_sec) { @@ -3164,6 +3218,10 @@ static int BrokerRetained_Store(MqttBroker* broker, const char* topic, if (broker == NULL || topic == NULL) { return MQTT_CODE_ERROR_BAD_ARG; } + + /* Drop expired entries first so they do not hold the cap against a new + * retained message. */ + BrokerRetained_ReapExpired(broker, WOLFMQTT_BROKER_GET_TIME_S()); #ifndef WOLFMQTT_STATIC_MEMORY cur = broker->retained; #endif diff --git a/src/mqtt_broker_persist.c b/src/mqtt_broker_persist.c index 502945aed..2fc416cf2 100644 --- a/src/mqtt_broker_persist.c +++ b/src/mqtt_broker_persist.c @@ -1251,6 +1251,7 @@ static int wmqb_decode_and_insert_retained(MqttBroker* broker, word64 store_time; word32 expiry; byte qos; + WOLFMQTT_BROKER_TIME_T now; rc = wmqb_read_header(blob, blob_len, BROKER_PERSIST_NS_RETAINED, &body_len); @@ -1271,6 +1272,14 @@ static int wmqb_decode_and_insert_retained(MqttBroker* broker, return MQTT_CODE_ERROR_MALFORMED_DATA; } + /* Skip records whose expiry already elapsed so they do not consume a + * retained slot / the cap at restore. Counted as skipped, not loaded. */ + now = WOLFMQTT_BROKER_GET_TIME_S(); + if (expiry > 0 && + (now - (WOLFMQTT_BROKER_TIME_T)store_time) >= expiry) { + return 1; + } + #ifdef WOLFMQTT_STATIC_MEMORY { int i; diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index 483519fdf..e6055ac53 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -1721,6 +1721,71 @@ TEST(broker_retained_list_capped) MqttBroker_Stop(&broker); MqttBroker_Free(&broker); } + +/* Expired retained messages must not hold the cap against a fresh retained + * publish. Pre-fix the cap was enforced before any expiry pruning, so a list + * full of expired entries rejected new retained topics with OUT_OF_MEMORY. */ +TEST(broker_retained_expired_freed_under_cap) +{ + MqttBroker broker; + MqttBrokerNet net; + BrokerRetainedMsg* rm; + int i; + byte pub[8]; + static const byte connect[] = { + 0x10, 0x0F, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x03, 'p', 'u', 'b' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(1); + mock_client_input_append(0, connect, sizeof(connect)); + /* Fill the cap with distinct retained topics. */ + for (i = 0; i < BROKER_MAX_RETAINED; i++) { + pub[0] = 0x31; /* PUBLISH, retain=1 */ + pub[1] = 0x06; + pub[2] = 0x00; pub[3] = 0x03; + pub[4] = 'r'; + pub[5] = (byte)('0' + (i / 10)); + pub[6] = (byte)('0' + (i % 10)); + pub[7] = 'x'; + mock_client_input_append(0, pub, sizeof(pub)); + } + for (i = 0; i < BROKER_MAX_RETAINED + 4; i++) { + MqttBroker_Step(&broker); + } + ASSERT_EQ(BROKER_MAX_RETAINED, broker.retained_count); + + /* Test time is pinned to 0, so a node stamped at tick 1 with a 1s expiry + * already reads as expired at tick 0 - force every entry expired. */ + for (rm = broker.retained; rm != NULL; rm = rm->next) { + rm->store_time = 1; + rm->expiry_sec = 1; + } + + /* A new retained topic must now be accepted, reaping the expired ones. */ + pub[0] = 0x31; + pub[1] = 0x06; + pub[2] = 0x00; pub[3] = 0x03; + pub[4] = 'n'; pub[5] = '0'; pub[6] = '0'; + pub[7] = 'y'; + mock_client_input_append(0, pub, sizeof(pub)); + for (i = 0; i < 4; i++) { + MqttBroker_Step(&broker); + } + + /* Expired entries freed, only the fresh topic remains. */ + ASSERT_EQ(1, broker.retained_count); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} #endif /* WOLFMQTT_BROKER_RETAINED && !WOLFMQTT_STATIC_MEMORY */ #ifndef WOLFMQTT_STATIC_MEMORY @@ -2702,6 +2767,7 @@ int main(int argc, char** argv) RUN_TEST(broker_publish_before_connect_closes); #if defined(WOLFMQTT_BROKER_RETAINED) && !defined(WOLFMQTT_STATIC_MEMORY) RUN_TEST(broker_retained_list_capped); + RUN_TEST(broker_retained_expired_freed_under_cap); #endif #ifndef WOLFMQTT_STATIC_MEMORY RUN_TEST(broker_per_client_subscription_cap); From 25a0fb95d9f3738e9fcc7aef898393e161661cee Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 16 Jun 2026 10:04:33 -0700 Subject: [PATCH 39/40] Guard retained-expiry and idle-timeout elapsed-time checks against backward clock steps --- src/mqtt_broker.c | 12 +++++++++--- src/mqtt_broker_persist.c | 5 ++++- tests/test_broker_connect.c | 22 ++++++++++++---------- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index f77a3481f..f9f7d60b2 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -3159,7 +3159,10 @@ static void BrokerRetained_ReapExpired(MqttBroker* broker, int i; for (i = 0; i < BROKER_MAX_RETAINED; i++) { BrokerRetainedMsg* rm = &broker->retained[i]; + /* now >= store_time guards the unsigned subtraction so a backward + * clock step reads as "not expired" instead of wrapping huge. */ if (rm->in_use && rm->expiry_sec > 0 && + now >= rm->store_time && (now - rm->store_time) >= rm->expiry_sec) { WBLOG_DBG(broker, "broker: retained expired topic=%s", BrokerLog_Sanitize(rm->topic)); @@ -3178,6 +3181,7 @@ static void BrokerRetained_ReapExpired(MqttBroker* broker, while (cur != NULL) { BrokerRetainedMsg* next = cur->next; if (cur->expiry_sec > 0 && + now >= cur->store_time && (now - cur->store_time) >= cur->expiry_sec) { WBLOG_DBG(broker, "broker: retained expired topic=%s", BrokerLog_Sanitize(cur->topic)); @@ -3815,8 +3819,9 @@ static void BrokerRetained_DeliverToClient(MqttBroker* broker, if (!rm->in_use || rm->topic[0] == '\0') { continue; } - /* Skip expired messages */ + /* Skip expired messages (now >= store_time guards clock rollback) */ if (rm->expiry_sec > 0 && + now >= rm->store_time && (now - rm->store_time) >= rm->expiry_sec) { WBLOG_DBG(broker, "broker: retained expired topic=%s", BrokerLog_Sanitize(rm->topic)); @@ -3861,6 +3866,7 @@ static void BrokerRetained_DeliverToClient(MqttBroker* broker, * mark the node and defer to the retained_delivering==0 post-loop reap. */ if (rm->pending_delete || (rm->expiry_sec > 0 && + now >= rm->store_time && (now - rm->store_time) >= rm->expiry_sec)) { if (broker->retained_delivering > 1) { rm->pending_delete = 1; @@ -5948,7 +5954,7 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) /* Check keepalive timeout (MQTT spec 3.1.2.10: 1.5x keep alive) */ if (bc->keep_alive_sec > 0) { WOLFMQTT_BROKER_TIME_T now = WOLFMQTT_BROKER_GET_TIME_S(); - if ((now - bc->last_rx) > + if (now >= bc->last_rx && (now - bc->last_rx) > (WOLFMQTT_BROKER_TIME_T)(bc->keep_alive_sec * 3 / 2)) { WBLOG_ERR(broker, "broker: keepalive timeout sock=%d", (int)bc->sock); #ifdef WOLFMQTT_V5 @@ -5973,7 +5979,7 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) * deadline (last_rx is the accept time until the first full packet) * so half-open sockets cannot squat the client table. */ WOLFMQTT_BROKER_TIME_T now = WOLFMQTT_BROKER_GET_TIME_S(); - if ((now - bc->last_rx) > + if (now >= bc->last_rx && (now - bc->last_rx) > (WOLFMQTT_BROKER_TIME_T)BROKER_CONNECT_TIMEOUT_SEC) { WBLOG_ERR(broker, "broker: pre-CONNECT idle timeout sock=%d", (int)bc->sock); diff --git a/src/mqtt_broker_persist.c b/src/mqtt_broker_persist.c index 2fc416cf2..f25e722c3 100644 --- a/src/mqtt_broker_persist.c +++ b/src/mqtt_broker_persist.c @@ -1273,9 +1273,12 @@ static int wmqb_decode_and_insert_retained(MqttBroker* broker, } /* Skip records whose expiry already elapsed so they do not consume a - * retained slot / the cap at restore. Counted as skipped, not loaded. */ + * retained slot / the cap at restore. Counted as skipped, not loaded. + * now >= store_time guards the unsigned subtraction against a backward + * clock step (mirrors the orphan-session restore sweep below). */ now = WOLFMQTT_BROKER_GET_TIME_S(); if (expiry > 0 && + now >= (WOLFMQTT_BROKER_TIME_T)store_time && (now - (WOLFMQTT_BROKER_TIME_T)store_time) >= expiry) { return 1; } diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index e6055ac53..5f33468a3 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -1722,10 +1722,12 @@ TEST(broker_retained_list_capped) MqttBroker_Free(&broker); } -/* Expired retained messages must not hold the cap against a fresh retained - * publish. Pre-fix the cap was enforced before any expiry pruning, so a list - * full of expired entries rejected new retained topics with OUT_OF_MEMORY. */ -TEST(broker_retained_expired_freed_under_cap) +/* A backward clock step must not make live retained messages look expired. + * Entries stamped in the "future" (store_time > now) would, with an unguarded + * unsigned subtraction, wrap to a huge elapsed value and be wrongly reaped; + * the now >= store_time guard keeps them. Test time is pinned to 0, so a node + * stamped at tick 1 models a clock that has since rolled back to 0. */ +TEST(broker_retained_clock_rollback_not_expired) { MqttBroker broker; MqttBrokerNet net; @@ -1762,14 +1764,15 @@ TEST(broker_retained_expired_freed_under_cap) } ASSERT_EQ(BROKER_MAX_RETAINED, broker.retained_count); - /* Test time is pinned to 0, so a node stamped at tick 1 with a 1s expiry - * already reads as expired at tick 0 - force every entry expired. */ + /* Stamp every entry in the future relative to the pinned now=0 clock. */ for (rm = broker.retained; rm != NULL; rm = rm->next) { rm->store_time = 1; rm->expiry_sec = 1; } - /* A new retained topic must now be accepted, reaping the expired ones. */ + /* A new retained topic triggers the reap path. The future-stamped entries + * must be kept (not falsely expired), so the cap still rejects the new + * topic and the count is unchanged. */ pub[0] = 0x31; pub[1] = 0x06; pub[2] = 0x00; pub[3] = 0x03; @@ -1780,8 +1783,7 @@ TEST(broker_retained_expired_freed_under_cap) MqttBroker_Step(&broker); } - /* Expired entries freed, only the fresh topic remains. */ - ASSERT_EQ(1, broker.retained_count); + ASSERT_EQ(BROKER_MAX_RETAINED, broker.retained_count); MqttBroker_Stop(&broker); MqttBroker_Free(&broker); @@ -2767,7 +2769,7 @@ int main(int argc, char** argv) RUN_TEST(broker_publish_before_connect_closes); #if defined(WOLFMQTT_BROKER_RETAINED) && !defined(WOLFMQTT_STATIC_MEMORY) RUN_TEST(broker_retained_list_capped); - RUN_TEST(broker_retained_expired_freed_under_cap); + RUN_TEST(broker_retained_clock_rollback_not_expired); #endif #ifndef WOLFMQTT_STATIC_MEMORY RUN_TEST(broker_per_client_subscription_cap); From 465598b6a9422d39985cecf1dcd29fc4b3578d6e Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 16 Jun 2026 10:32:37 -0700 Subject: [PATCH 40/40] Reject retained-store-failed publishes with v5 quota reason and close clients on property protocol errors --- src/mqtt_broker.c | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index f9f7d60b2..525e1ca5a 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -5131,6 +5131,9 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, MqttPublishResp resp; byte* payload = NULL; char* topic = NULL; +#if defined(WOLFMQTT_V5) && defined(WOLFMQTT_BROKER_RETAINED) + int retain_rc = MQTT_CODE_SUCCESS; +#endif #if WOLFMQTT_MAX_QOS >= 2 int qos2_duplicate = 0; #endif @@ -5309,6 +5312,9 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, WBLOG_ERR(broker, "Retained store failed: %s", MqttClient_ReturnCodeToString(ret_rc)); } +#ifdef WOLFMQTT_V5 + retain_rc = ret_rc; +#endif } } } @@ -5480,7 +5486,14 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, resp.packet_id = pub.packet_id; #ifdef WOLFMQTT_V5 resp.protocol_level = bc->protocol_level; + /* A retained-store failure (cap reached / OOM) must not be ACKed as + * success: tell the publisher the quota was exceeded [MQTT-3.4.2]. */ +#ifdef WOLFMQTT_BROKER_RETAINED + resp.reason_code = (retain_rc != MQTT_CODE_SUCCESS) + ? MQTT_REASON_QUOTA_EXCEEDED : MQTT_REASON_SUCCESS; +#else resp.reason_code = MQTT_REASON_SUCCESS; +#endif resp.props = NULL; #endif rc = MqttEncode_PublishResp(bc->tx_buf, BROKER_CLIENT_TX_SZ(bc), @@ -5618,12 +5631,16 @@ static void BrokerClient_AbnormalClose(MqttBroker* broker, BrokerClient* bc) * [MQTT-4.13]/[MQTT-4.8.0-1] mandate connection close on malformed * packets. * - Server-side resource exhaustion (allocator failure, per-client cap - * reached) - the connection must be torn down so resources release. */ + * reached) - the connection must be torn down so resources release. + * - v5 property protocol errors (too many properties, duplicate singleton) + * are malformed packets [MQTT-4.13.1]; the client must be disconnected. */ static int BrokerRcIsFatal(int rc) { return (rc == MQTT_CODE_ERROR_MALFORMED_DATA || rc == MQTT_CODE_ERROR_PACKET_TYPE || rc == MQTT_CODE_ERROR_PACKET_ID || + rc == MQTT_CODE_ERROR_PROPERTY || + rc == MQTT_CODE_ERROR_PROPERTY_MISMATCH || rc == MQTT_CODE_ERROR_MEMORY || rc == MQTT_CODE_ERROR_OUT_OF_BUFFER); }