@@ -772,6 +772,103 @@ void test_E9_decoded_partial_signature_from_nonsigner_dropped(void)
772772 TEST_ASSERT_FALSE_MESSAGE (checkXeddsaReceivePolicy (&p), " partial signature must be dropped as malformed" );
773773}
774774
775+ // Build an unsigned broadcast whose inner message is padded with an unknown field, and pin that the
776+ // padding pushes the RAW size past the fit threshold - the exemption the attacker is buying - while
777+ // the frame stays sendable. Without canonical inner sizing these packets are wrongly accepted.
778+ static meshtastic_MeshPacket makePayloadPaddedBroadcast (meshtastic_PortNum port, const pb_msgdesc_t *fields, const void *inner,
779+ size_t padLen)
780+ {
781+ meshtastic_MeshPacket p = makeDecoded (REMOTE_NODE , NODENUM_BROADCAST , port, 0 );
782+ const size_t innerLen = pb_encode_to_bytes (p.decoded .payload .bytes , sizeof (p.decoded .payload .bytes ), fields, inner);
783+ TEST_ASSERT_GREATER_THAN_MESSAGE (0 , innerLen, " failed to encode the spoofed inner message" );
784+ p.decoded .payload .size =
785+ innerLen + appendUnknownField (p.decoded .payload .bytes + innerLen, sizeof (p.decoded .payload .bytes ) - innerLen, padLen);
786+
787+ TEST_ASSERT_FALSE_MESSAGE (signedEncodingFits (&p.decoded ), " padding must push the raw size past the fit threshold" );
788+ TEST_ASSERT_LESS_OR_EQUAL_MESSAGE (MAX_LORA_PAYLOAD_LEN , encodedDataSize (&p.decoded ) + MESHTASTIC_HEADER_LENGTH ,
789+ " padded frame must still be one a radio could send" );
790+ return p;
791+ }
792+
793+ // E10: unknown fields buried inside Data.payload are discarded by the module's own pb_decode, so
794+ // they must not sway the downgrade decision the way A10 already pins for Data-level unknown fields.
795+ void test_E10_decoded_unsigned_position_padded_inside_payload_dropped (void )
796+ {
797+ mockNodeDB->addNode (REMOTE_NODE );
798+ mockNodeDB->setSignerBit (REMOTE_NODE , true );
799+
800+ meshtastic_Position pos = meshtastic_Position_init_zero;
801+ pos.has_latitude_i = pos.has_longitude_i = true ;
802+ pos.latitude_i = 371234567 ;
803+ pos.longitude_i = -1221234567 ;
804+
805+ meshtastic_MeshPacket p = makePayloadPaddedBroadcast (meshtastic_PortNum_POSITION_APP, &meshtastic_Position_msg, &pos, 163 );
806+
807+ TEST_ASSERT_FALSE_MESSAGE (checkXeddsaReceivePolicy (&p), " payload-padded unsigned Position from a signer must be dropped" );
808+ TEST_ASSERT_FALSE (p.xeddsa_signed );
809+ }
810+
811+ // E11: over-correction guard. Telemetry (272 bytes max) and Waypoint (199) can legitimately exceed
812+ // the signable budget, so canonical sizing must not shrink an honest one into the drop range.
813+ void test_E11_decoded_unsigned_oversized_telemetry_from_signer_accepted (void )
814+ {
815+ mockNodeDB->addNode (REMOTE_NODE );
816+ mockNodeDB->setSignerBit (REMOTE_NODE , true );
817+
818+ meshtastic_Telemetry t = meshtastic_Telemetry_init_zero;
819+ t.which_variant = meshtastic_Telemetry_host_metrics_tag;
820+ t.variant .host_metrics .uptime_seconds = 123456 ;
821+ t.variant .host_metrics .has_user_string = true ;
822+ memset (t.variant .host_metrics .user_string , ' x' , sizeof (t.variant .host_metrics .user_string ) - 1 );
823+
824+ meshtastic_MeshPacket p = makeDecoded (REMOTE_NODE , NODENUM_BROADCAST , meshtastic_PortNum_TELEMETRY_APP, 0 );
825+ p.decoded .payload .size =
826+ pb_encode_to_bytes (p.decoded .payload .bytes , sizeof (p.decoded .payload .bytes ), &meshtastic_Telemetry_msg, &t);
827+ TEST_ASSERT_GREATER_THAN_MESSAGE (0 , p.decoded .payload .size , " failed to encode the oversized Telemetry" );
828+
829+ // Every byte here is a field this build understands, so canonical sizing must leave it alone.
830+ TEST_ASSERT_FALSE_MESSAGE (signedEncodingFits (&p.decoded ), " telemetry must be too big to sign, else the test is vacuous" );
831+
832+ TEST_ASSERT_TRUE_MESSAGE (checkXeddsaReceivePolicy (&p), " honest oversized telemetry from a signer must not be dropped" );
833+ }
834+
835+ // E12: E10 for the Waypoint branch of the canonical-sizing switch.
836+ void test_E12_decoded_unsigned_waypoint_padded_inside_payload_dropped (void )
837+ {
838+ mockNodeDB->addNode (REMOTE_NODE );
839+ mockNodeDB->setSignerBit (REMOTE_NODE , true );
840+
841+ meshtastic_Waypoint w = meshtastic_Waypoint_init_zero;
842+ w.id = 42 ;
843+ w.has_latitude_i = w.has_longitude_i = true ;
844+ w.latitude_i = 371234567 ;
845+ w.longitude_i = -1221234567 ;
846+ strcpy (w.name , " spoofed" );
847+
848+ meshtastic_MeshPacket p = makePayloadPaddedBroadcast (meshtastic_PortNum_WAYPOINT_APP, &meshtastic_Waypoint_msg, &w, 150 );
849+
850+ TEST_ASSERT_FALSE_MESSAGE (checkXeddsaReceivePolicy (&p), " payload-padded unsigned Waypoint from a signer must be dropped" );
851+ TEST_ASSERT_FALSE (p.xeddsa_signed );
852+ }
853+
854+ // E13: E10 for the NodeInfo/User branch. Router drops it before rebroadcast; NodeInfoModule's own
855+ // check (Group C) is receiver-local and would not stop the packet propagating.
856+ void test_E13_decoded_unsigned_nodeinfo_padded_inside_payload_dropped (void )
857+ {
858+ mockNodeDB->addNode (REMOTE_NODE );
859+ mockNodeDB->setSignerBit (REMOTE_NODE , true );
860+
861+ meshtastic_User u = meshtastic_User_init_zero;
862+ strcpy (u.id , " !0b0b0b0b" );
863+ strcpy (u.long_name , " spoofed node" );
864+ strcpy (u.short_name , " SPF" );
865+
866+ meshtastic_MeshPacket p = makePayloadPaddedBroadcast (meshtastic_PortNum_NODEINFO_APP, &meshtastic_User_msg, &u, 150 );
867+
868+ TEST_ASSERT_FALSE_MESSAGE (checkXeddsaReceivePolicy (&p), " payload-padded unsigned NodeInfo from a signer must be dropped" );
869+ TEST_ASSERT_FALSE (p.xeddsa_signed );
870+ }
871+
775872void setup ()
776873{
777874 initializeTestEnvironment ();
@@ -817,6 +914,10 @@ void setup()
817914 RUN_TEST (test_E7_decoded_unsigned_pki_from_signer_accepted);
818915 RUN_TEST (test_E8_decoded_partial_signature_from_signer_dropped);
819916 RUN_TEST (test_E9_decoded_partial_signature_from_nonsigner_dropped);
917+ RUN_TEST (test_E10_decoded_unsigned_position_padded_inside_payload_dropped);
918+ RUN_TEST (test_E11_decoded_unsigned_oversized_telemetry_from_signer_accepted);
919+ RUN_TEST (test_E12_decoded_unsigned_waypoint_padded_inside_payload_dropped);
920+ RUN_TEST (test_E13_decoded_unsigned_nodeinfo_padded_inside_payload_dropped);
820921
821922 exit (UNITY_END ());
822923}
0 commit comments