Commit 68ec688eb6 for freeswitch.com

commit 68ec688eb601767a28854e804a0e4df5872f5bc7
Author: Dmitry Verenitsin <morbit85@gmail.com>
Date:   Sat Aug 8 19:03:47 2026 +0500

    Merge commit from fork

    Add `switch_stun_packet_verify_integrity()`, an HMAC-SHA1
    MESSAGE-INTEGRITY verifier that is const and non-mutating: it runs
    over a private copy of the pristine network-order packet, so the
    caller's buffer and byte order stay untouched, and walks attributes
    with its own unsigned bounded helper `stun_wire_attr_bounds()`
    instead of the host-order iterator macros. A trailing
    MESSAGE-INTEGRITY-SHA256 or FINGERPRINT after MESSAGE-INTEGRITY is
    tolerated; any other trailing attribute is rejected.

    Gate it in `handle_ice()` behind `ice->verify_integrity`: verify
    before any ICE state is touched, keyed by message type (local
    `ice->pass` for a request, remote `ice->rpass` for a response or
    error response), and drop on failure. Keepalive indications carry no
    MESSAGE-INTEGRITY and are ignored.

    `ice->verify_integrity` is read from the `ice_verify_message_integrity`
    channel variable in `switch_rtp_activate_ice()` and defaults off, so
    receive-path behavior is unchanged unless it is enabled. Adds unit
    tests in `tests/unit/switch_stun.c`.

diff --git a/conf/vanilla/vars.xml b/conf/vanilla/vars.xml
index ae7c355f99..c65d7c25d3 100644
--- a/conf/vanilla/vars.xml
+++ b/conf/vanilla/vars.xml
@@ -426,6 +426,10 @@
   <X-PRE-PROCESS cmd="set" data="rtp_liberal_dtmf=true"/>
   <!-- Helps with WebRTC Audio -->

+  <!-- Require valid MESSAGE-INTEGRITY on inbound ICE (STUN) connectivity checks; unverified checks are
+       dropped. Default off. Uncomment and change Z- prefix to X- to enable. -->
+  <!--<Z-PRE-PROCESS cmd="set" data="ice_verify_message_integrity=true"/>-->
+
   <!-- Stock Video Avatars -->
   <X-PRE-PROCESS cmd="set" data="video_mute_png=$${images_dir}/default-mute.png"/>
   <X-PRE-PROCESS cmd="set" data="video_no_avatar_png=$${images_dir}/default-avatar.png"/>
diff --git a/src/include/switch_stun.h b/src/include/switch_stun.h
index ca612c06aa..a2d6f9b2cd 100644
--- a/src/include/switch_stun.h
+++ b/src/include/switch_stun.h
@@ -79,6 +79,7 @@ typedef enum {
 	SWITCH_STUN_ATTR_DESTINATION_ADDRESS = 0x0011,	/* Address */
 	SWITCH_STUN_ATTR_SOURCE_ADDRESS2 = 0x0012,	/* Address */
 	SWITCH_STUN_ATTR_DATA = 0x0013,	/* ByteString */
+	SWITCH_STUN_ATTR_MESSAGE_INTEGRITY_SHA256 = 0x001c,	/* ByteString, 16-32 bytes (RFC 8489) */
 	SWITCH_STUN_ATTR_OPTIONS = 0x8001,	/* UInt32 */
 	SWITCH_STUN_ATTR_XOR_MAPPED_ADDRESS = 0x0020,   /* Address */

@@ -246,6 +247,19 @@ SWITCH_DECLARE(uint8_t) switch_stun_packet_attribute_add_software(switch_stun_pa
 SWITCH_DECLARE(uint8_t) switch_stun_packet_attribute_add_binded_address(switch_stun_packet_t *packet, char *ipstr, uint16_t port, int family);
 SWITCH_DECLARE(uint8_t) switch_stun_packet_attribute_add_xor_binded_address(switch_stun_packet_t *packet, char *ipstr, uint16_t port, int family);
 SWITCH_DECLARE(uint8_t) switch_stun_packet_attribute_add_integrity(switch_stun_packet_t *packet, const char *pass);
+
+/*!
+  \brief Verify the MESSAGE-INTEGRITY (HMAC-SHA1) of a received STUN packet
+  \param pkt the raw, unmodified (network byte order) packet bytes as received off the wire
+  \param len the number of valid bytes in pkt
+  \param pass the key (ICE password) the sender is expected to have used
+  \return SWITCH_STATUS_SUCCESS if a MESSAGE-INTEGRITY attribute is present and its HMAC matches,
+          SWITCH_STATUS_NOTFOUND if no MESSAGE-INTEGRITY attribute is present,
+          SWITCH_STATUS_FALSE on a mismatch or a malformed/out-of-bounds packet
+  \note pkt must be the pristine wire bytes; switch_stun_packet_parse() rewrites fields to host
+        byte order in place, so a parsed buffer cannot be verified.
+*/
+SWITCH_DECLARE(switch_status_t) switch_stun_packet_verify_integrity(const uint8_t *pkt, uint32_t len, const char *pass);
 SWITCH_DECLARE(uint32_t) switch_crc32_8bytes(const void* data, size_t length);
 SWITCH_DECLARE(uint8_t) switch_stun_packet_attribute_add_fingerprint(switch_stun_packet_t *packet);
 SWITCH_DECLARE(uint8_t) switch_stun_packet_attribute_add_use_candidate(switch_stun_packet_t *packet);
diff --git a/src/switch_rtp.c b/src/switch_rtp.c
index d92b23001e..cf3c55f9d6 100644
--- a/src/switch_rtp.c
+++ b/src/switch_rtp.c
@@ -263,6 +263,7 @@ typedef struct {
 	char last_sent_id[13];
 	switch_time_t last_ok;
 	uint8_t cand_responsive;
+	uint8_t verify_integrity;
 } switch_rtp_ice_t;

 struct switch_rtp;
@@ -1010,6 +1011,36 @@ static void handle_ice(switch_rtp_t *rtp_session, switch_rtp_ice_t *ice, void *d

 	}

+	if ((ice->type & ICE_VANILLA) && ice->verify_integrity) {
+		/* Verify before any ICE state is touched, over the pristine wire bytes (not the byte-swapped
+		   host-order buf). Key by type: request with our local password, response/error-response with
+		   the remote password. Indications carry no MESSAGE-INTEGRITY and drive no state, so drop them. */
+		const char *ikey = NULL;
+
+		switch (packet->header.type) {
+		case SWITCH_STUN_BINDING_REQUEST:
+			ikey = ice->pass;
+			break;
+		case SWITCH_STUN_BINDING_RESPONSE:
+		case SWITCH_STUN_BINDING_ERROR_RESPONSE:
+			ikey = ice->rpass;
+			break;
+		default:
+			switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(rtp_session->session), SWITCH_LOG_DEBUG8,
+							  "%s ignoring unauthenticated STUN %s from %s:%d\n", rtp_type(rtp_session),
+							  switch_stun_value_to_name(SWITCH_STUN_TYPE_PACKET_TYPE, packet->header.type), from_host, from_port);
+			goto end;
+		}
+
+		if (switch_stun_packet_verify_integrity((const uint8_t *)data, (uint32_t)cpylen, ikey) != SWITCH_STATUS_SUCCESS) {
+			switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(rtp_session->session), SWITCH_LOG_WARNING,
+							  "%s STUN MESSAGE-INTEGRITY verification failed; dropping %s from %s:%d\n",
+							  rtp_type(rtp_session),
+							  switch_stun_value_to_name(SWITCH_STUN_TYPE_PACKET_TYPE, packet->header.type), from_host, from_port);
+			goto end;
+		}
+	}
+
 	rtp_session->last_stun = switch_micro_time_now();

 	if (!rtp_session->first_stun) {
@@ -4981,6 +5012,7 @@ SWITCH_DECLARE(switch_status_t) switch_rtp_activate_ice(switch_rtp_t *rtp_sessio
 	ice->ice_params = ice_params;
 	ice->pass = "";
 	ice->rpass = "";
+	ice->verify_integrity = 0;
 	ice->next_run = switch_micro_time_now();
 	ice->initializing = 1;

@@ -4992,6 +5024,10 @@ SWITCH_DECLARE(switch_status_t) switch_rtp_activate_ice(switch_rtp_t *rtp_sessio
 		ice->rpass = switch_core_strdup(rtp_session->pool, rpassword);
 	}

+	if ((type & ICE_VANILLA) && switch_channel_var_true(switch_core_session_get_channel(rtp_session->session), "ice_verify_message_integrity")) {
+		ice->verify_integrity = 1;
+	}
+
 	if ((ice->type & ICE_VANILLA) && ice->ice_params) {
 		host = ice->ice_params->cands[ice->ice_params->chosen[ice->proto]][ice->proto].con_addr;
 		port = ice->ice_params->cands[ice->ice_params->chosen[ice->proto]][ice->proto].con_port;
diff --git a/src/switch_stun.c b/src/switch_stun.c
index 1c0027e1a1..689990e8e3 100644
--- a/src/switch_stun.c
+++ b/src/switch_stun.c
@@ -35,6 +35,7 @@
 #include <switch_stun.h>
 #include <openssl/sha.h>
 #include <openssl/hmac.h>
+#include <openssl/crypto.h>

 struct value_mapping {
 	const uint32_t value;
@@ -705,6 +706,106 @@ SWITCH_DECLARE(uint8_t) switch_stun_packet_attribute_add_password(switch_stun_pa
 	return 1;
 }

+/* True if the attribute at `attr` fits within `end`, writing its padded value length to *padded.
+   Unsigned (not the int16_t macros) so a high-bit length can't go negative and walk backward. */
+static switch_bool_t stun_wire_attr_bounds(const switch_stun_packet_attribute_t *attr, const uint8_t *end, uint32_t *padded)
+{
+	uint32_t plen;
+
+	if ((const uint8_t *) (attr + 1) > end || !attr->type) {
+		return SWITCH_FALSE;
+	}
+
+	plen = ntohs(attr->length);
+	/* STUN pads each attribute value to a 4-byte boundary; round the declared length up to match. */
+	plen = (plen & 0x3) ? (plen & ~0x3u) + 4 : plen;
+
+	if (plen > (uint32_t) (end - (const uint8_t *) attr->value)) {
+		return SWITCH_FALSE;
+	}
+
+	*padded = plen;
+
+	return SWITCH_TRUE;
+}
+
+SWITCH_DECLARE(switch_status_t) switch_stun_packet_verify_integrity(const uint8_t *pkt, uint32_t len, const char *pass)
+{
+	uint8_t copy[1500];
+	switch_stun_packet_t *packet;
+	switch_stun_packet_attribute_t *attr;
+	switch_stun_packet_attribute_t *mi = NULL;
+	uint8_t *end;
+	uint32_t declared;
+	uint32_t mi_off;
+	uint32_t padded;
+	uint16_t hashed_length;
+	unsigned char digest[SHA_DIGEST_LENGTH];
+
+	if (!pkt || zstr(pass) || len < SWITCH_STUN_PACKET_MIN_LEN || len > sizeof(copy)) {
+		return SWITCH_STATUS_FALSE;
+	}
+
+	/* Work on a private copy so the caller's wire bytes stay untouched and we can safely
+	   rewrite the length field for the HMAC input. Input is network byte order. */
+	memcpy(copy, pkt, len);
+	packet = (switch_stun_packet_t *) copy;
+
+	/* Bound the attribute walk to the declared message length, clamped to what we actually
+	   received, so trailing bytes past the message are never treated as attributes. */
+	declared = SWITCH_STUN_PACKET_MIN_LEN + ntohs(packet->header.length);
+	end = copy + (declared < len ? declared : len);
+
+	/* Walk attributes (network byte order) to locate MESSAGE-INTEGRITY; stun_wire_attr_bounds
+	   bounds-checks each attribute, including the first. */
+	switch_stun_packet_first_attribute(packet, attr);
+	while (stun_wire_attr_bounds(attr, end, &padded)) {
+		if (attr->type == htons(SWITCH_STUN_ATTR_MESSAGE_INTEGRITY)) {
+			mi = attr;
+			break;
+		}
+
+		attr = (switch_stun_packet_attribute_t *) ((uint8_t *) attr->value + padded);
+	}
+
+	if (!mi) {
+		return SWITCH_STATUS_NOTFOUND;
+	}
+
+	/* MESSAGE-INTEGRITY always carries a 20-byte HMAC-SHA1 value. */
+	if (ntohs(mi->length) != SHA_DIGEST_LENGTH) {
+		return SWITCH_STATUS_FALSE;
+	}
+
+	mi_off = (uint32_t) ((uint8_t *) mi - copy);
+
+	/* Only MESSAGE-INTEGRITY-SHA256 and FINGERPRINT may follow MESSAGE-INTEGRITY; both sit outside this
+	   HMAC's coverage. Reject any other trailing attribute rather than act on it unauthenticated. */
+	attr = (switch_stun_packet_attribute_t *) (mi->value + SHA_DIGEST_LENGTH);
+	while (stun_wire_attr_bounds(attr, end, &padded)) {
+		if (attr->type != htons(SWITCH_STUN_ATTR_FINGERPRINT) &&
+			attr->type != htons(SWITCH_STUN_ATTR_MESSAGE_INTEGRITY_SHA256)) {
+			return SWITCH_STATUS_FALSE;
+		}
+
+		attr = (switch_stun_packet_attribute_t *) ((uint8_t *) attr->value + padded);
+	}
+
+	/* Reproduce the sender's HMAC input: header.length must read as if the message ended right
+	   after MESSAGE-INTEGRITY (so a trailing FINGERPRINT is excluded), and the HMAC covers the
+	   message prefix up to but not including the MESSAGE-INTEGRITY attribute. */
+	hashed_length = (uint16_t) (mi_off - sizeof(switch_stun_packet_header_t) + sizeof(switch_stun_packet_attribute_t) + SHA_DIGEST_LENGTH);
+	packet->header.length = htons(hashed_length);
+
+	HMAC(EVP_sha1(), (const unsigned char *) pass, (int) strlen(pass), copy, mi_off, digest, NULL);
+
+	if (CRYPTO_memcmp(digest, mi->value, SHA_DIGEST_LENGTH) != 0) {
+		return SWITCH_STATUS_FALSE;
+	}
+
+	return SWITCH_STATUS_SUCCESS;
+}
+
 SWITCH_DECLARE(char *) switch_stun_host_lookup(const char *host, switch_memory_pool_t *pool)
 {
 	switch_sockaddr_t *addr = NULL;
diff --git a/tests/unit/switch_stun.c b/tests/unit/switch_stun.c
index 675333c029..0948e795d9 100644
--- a/tests/unit/switch_stun.c
+++ b/tests/unit/switch_stun.c
@@ -165,6 +165,153 @@ FST_TEARDOWN_END()
 		fst_check_string_equals(out_ip, ipv4_str);
 	}
 	FST_TEST_END()
+
+	FST_TEST_BEGIN(test_stun_verify_integrity_accepts_valid_hmac)
+	{
+		/* A packet signed with add_integrity under a given key must verify against that same key,
+		   including when a leading attribute precedes MESSAGE-INTEGRITY (it is part of the HMAC input). */
+		uint8_t buf[512] = { 0 };
+		char software[] = "sw";
+		switch_stun_packet_t *packet;
+		uint32_t len;
+
+		packet = switch_stun_packet_build_header(SWITCH_STUN_BINDING_REQUEST, NULL, buf);
+		switch_stun_packet_attribute_add_software(packet, software, (uint16_t)strlen(software));
+		switch_stun_packet_attribute_add_integrity(packet, "secret");
+		len = (uint32_t)switch_stun_packet_length(packet);
+
+		fst_xcheck(switch_stun_packet_verify_integrity(buf, len, "secret") == SWITCH_STATUS_SUCCESS,
+				   "valid MESSAGE-INTEGRITY verifies against the signing key");
+		fst_xcheck(switch_stun_packet_verify_integrity(buf, len, "wrong") == SWITCH_STATUS_FALSE,
+				   "MESSAGE-INTEGRITY does not verify against a different key");
+	}
+	FST_TEST_END()
+
+	FST_TEST_BEGIN(test_stun_verify_integrity_rejects_zeroed_hmac)
+	{
+		/* An all-zero MESSAGE-INTEGRITY value must not verify: this is the shape a sender produces when
+		   it fills the field with zeros instead of computing the HMAC. MI is the first attribute, so its
+		   20-byte value sits at offset 24 (20-byte header plus 4-byte attribute header). */
+		uint8_t buf[512] = { 0 };
+		switch_stun_packet_t *packet;
+		uint32_t len;
+
+		packet = switch_stun_packet_build_header(SWITCH_STUN_BINDING_REQUEST, NULL, buf);
+		switch_stun_packet_attribute_add_integrity(packet, "secret");
+		len = (uint32_t)switch_stun_packet_length(packet);
+		memset(buf + SWITCH_STUN_PACKET_MIN_LEN + 4, 0, 20);
+
+		fst_xcheck(switch_stun_packet_verify_integrity(buf, len, "secret") == SWITCH_STATUS_FALSE,
+				   "a zeroed MESSAGE-INTEGRITY value is rejected");
+	}
+	FST_TEST_END()
+
+	FST_TEST_BEGIN(test_stun_verify_integrity_trailing_fingerprint)
+	{
+		/* MESSAGE-INTEGRITY followed by FINGERPRINT (the layout our own responses use) must still verify:
+		   the HMAC input's length field reads as if the message ended right after MESSAGE-INTEGRITY, so the
+		   trailing FINGERPRINT is excluded from the computation. */
+		uint8_t buf[512] = { 0 };
+		switch_stun_packet_t *packet;
+		uint32_t len;
+
+		packet = switch_stun_packet_build_header(SWITCH_STUN_BINDING_RESPONSE, NULL, buf);
+		switch_stun_packet_attribute_add_integrity(packet, "secret");
+		switch_stun_packet_attribute_add_fingerprint(packet);
+		len = (uint32_t)switch_stun_packet_length(packet);
+
+		fst_xcheck(switch_stun_packet_verify_integrity(buf, len, "secret") == SWITCH_STATUS_SUCCESS,
+				   "MESSAGE-INTEGRITY verifies with a trailing FINGERPRINT present");
+	}
+	FST_TEST_END()
+
+	FST_TEST_BEGIN(test_stun_verify_integrity_absent)
+	{
+		/* A packet with no MESSAGE-INTEGRITY attribute reports NOTFOUND, distinct from a mismatch, so the
+		   caller can apply its own present-or-absent policy. */
+		uint8_t buf[512] = { 0 };
+		char software[] = "sw";
+		switch_stun_packet_t *packet;
+		uint32_t len;
+
+		packet = switch_stun_packet_build_header(SWITCH_STUN_BINDING_REQUEST, NULL, buf);
+		switch_stun_packet_attribute_add_software(packet, software, (uint16_t)strlen(software));
+		len = (uint32_t)switch_stun_packet_length(packet);
+
+		fst_xcheck(switch_stun_packet_verify_integrity(buf, len, "secret") == SWITCH_STATUS_NOTFOUND,
+				   "a packet with no MESSAGE-INTEGRITY reports NOTFOUND");
+	}
+	FST_TEST_END()
+
+	FST_TEST_BEGIN(test_stun_verify_integrity_rejects_attr_after_mi)
+	{
+		/* Only MESSAGE-INTEGRITY-SHA256 and FINGERPRINT may follow MESSAGE-INTEGRITY. A USE-CANDIDATE
+		   appended after MI leaves the HMAC prefix - and therefore the signature - valid, so it must be
+		   rejected outright rather than verified and then acted on. */
+		uint8_t buf[512] = { 0 };
+		switch_stun_packet_t *packet;
+		uint32_t len;
+
+		packet = switch_stun_packet_build_header(SWITCH_STUN_BINDING_REQUEST, NULL, buf);
+		switch_stun_packet_attribute_add_integrity(packet, "secret");
+		switch_stun_packet_attribute_add_use_candidate(packet);	/* lands after MESSAGE-INTEGRITY */
+		len = (uint32_t)switch_stun_packet_length(packet);
+
+		fst_xcheck(switch_stun_packet_verify_integrity(buf, len, "secret") == SWITCH_STATUS_FALSE,
+				   "a non-FINGERPRINT attribute after MESSAGE-INTEGRITY is rejected");
+	}
+	FST_TEST_END()
+
+	FST_TEST_BEGIN(test_stun_verify_integrity_oversized_attr_length)
+	{
+		/* An attribute length with the high bit set must not walk the cursor backward or read out of
+		   bounds: the walk treats the padded length as unsigned and stops once it exceeds the bytes that
+		   remain. With a leading oversized attribute, MESSAGE-INTEGRITY is never reached (NOTFOUND) and no
+		   out-of-bounds access occurs (ASAN would catch a regression here). */
+		uint8_t buf[64] = { 0 };
+		switch_stun_packet_t *packet;
+		switch_stun_packet_attribute_t *attr;
+		uint32_t len;
+
+		packet = switch_stun_packet_build_header(SWITCH_STUN_BINDING_REQUEST, NULL, buf);
+		attr = (switch_stun_packet_attribute_t *)packet->first_attribute;
+		attr->type = htons(SWITCH_STUN_ATTR_USERNAME);
+		attr->length = htons(0x8000);
+		packet->header.length = htons(4);	/* declare just the 4-byte attribute header */
+		len = SWITCH_STUN_PACKET_MIN_LEN + 4;
+
+		fst_xcheck(switch_stun_packet_verify_integrity(buf, len, "secret") == SWITCH_STATUS_NOTFOUND,
+				   "an attribute length with the high bit set stops the walk without an out-of-bounds read");
+	}
+	FST_TEST_END()
+
+	FST_TEST_BEGIN(test_stun_verify_integrity_allows_sha256_after_mi)
+	{
+		/* RFC 8489 permits MESSAGE-INTEGRITY-SHA256 to follow MESSAGE-INTEGRITY. It is inert (carries no
+		   ICE state) and outside the SHA-1 HMAC's coverage, so a dual-hash sender's packet must still
+		   verify on its SHA-1 MESSAGE-INTEGRITY rather than be rejected as a disallowed trailing attribute. */
+		uint8_t buf[512] = { 0 };
+		switch_stun_packet_t *packet;
+		switch_stun_packet_attribute_t *sha256;
+		uint32_t off;
+		uint32_t len;
+
+		packet = switch_stun_packet_build_header(SWITCH_STUN_BINDING_REQUEST, NULL, buf);
+		switch_stun_packet_attribute_add_integrity(packet, "secret");
+
+		/* Append a MESSAGE-INTEGRITY-SHA256 attribute (32-byte value, left zero: the SHA-1 verifier does
+		   not inspect it) immediately after MESSAGE-INTEGRITY. */
+		off = SWITCH_STUN_PACKET_MIN_LEN + ntohs(packet->header.length);
+		sha256 = (switch_stun_packet_attribute_t *)(buf + off);
+		sha256->type = htons(SWITCH_STUN_ATTR_MESSAGE_INTEGRITY_SHA256);
+		sha256->length = htons(32);
+		packet->header.length = htons((uint16_t)(ntohs(packet->header.length) + 4 + 32));
+		len = (uint32_t)switch_stun_packet_length(packet);
+
+		fst_xcheck(switch_stun_packet_verify_integrity(buf, len, "secret") == SWITCH_STATUS_SUCCESS,
+				   "a MESSAGE-INTEGRITY-SHA256 attribute after MESSAGE-INTEGRITY is tolerated");
+	}
+	FST_TEST_END()
 }
 FST_SUITE_END()
 }