Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,31 @@ Uses `.clang-format` with LLVM base style:
- Tab indentation (4-space tabs)
- K&R inspired style

## Test Integrity
Never modify, delete, skip, or weaken tests to make them pass.
Never hardcode expected values, mock results, or otherwise contrive a passing test result.
Comment thread
embhorn marked this conversation as resolved.
Outdated
A passing test suite achieved by changing the tests (not the implementation) is not a passing result.
Fix the code. If the code cannot be fixed within scope, escalate.

Never write a test that uses the code under test as its own oracle. Tests must have an independent oracle: known test vectors from an external source, cross-validation between two independent implementations, or bit-exact comparison against a reference path. A test that encrypts with function A and decrypts with function A proves nothing.

Comment thread
embhorn marked this conversation as resolved.
Outdated
## No Fabrication
Never report status, results, or completion that does not reflect work actually performed.
If you are uncertain whether a step succeeded, say so explicitly. Do not paper over uncertainty with confident-sounding output.

## Exit Code Discipline
Every shell command's exit code must be checked.
Never proceed after a silent failure.
A command that failed and was ignored is not a completed step.

### Test vector discipline
Comment thread
embhorn marked this conversation as resolved.
Outdated
Never derive test vectors from the code under test. Acceptable oracles:
- OpenSSL (`openssl kdf`, `openssl enc`, `openssl pkcs8 ... | sha256sum`)
- pyca/cryptography (`pkcs12.load_key_and_certificates`, `private_bytes(DER, PKCS8, NoEncryption)`)
- Bouncy Castle test vectors
Compute once, hardcode as `hex!(...)` literals or committed binary fixtures. Tests must be fully offline.
For PKCS12KDF: use `hexpass:` (BMP/UTF-16BE + null terminator), NOT `pass:` (raw ASCII).

## Dependencies

- **wolfSSL** - Required for TLS support
Expand Down
276 changes: 276 additions & 0 deletions tests/test_mqtt_packet.c
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,207 @@ TEST(encode_publish_qos1_valid)
ASSERT_TRUE(rc > 0);
}

/* Verify the fixed-header flag bits (retain/QoS/dup) are actually emitted.
* Covers deletion mutations of the retain / qos / duplicate branches in
* MqttEncode_FixedHeader. */
TEST(encode_publish_qos1_retain_flags_in_header)
{
byte tx_buf[256];
MqttPublish pub;
int rc;

XMEMSET(&pub, 0, sizeof(pub));
pub.topic_name = "test/topic";
pub.qos = MQTT_QOS_1;
pub.retain = 1;
pub.duplicate = 0;
pub.packet_id = 1;
rc = MqttEncode_Publish(tx_buf, (int)sizeof(tx_buf), &pub, 0);
ASSERT_TRUE(rc > 0);
ASSERT_EQ(MQTT_PACKET_TYPE_PUBLISH,
MQTT_PACKET_TYPE_GET(tx_buf[0]));
ASSERT_EQ(MQTT_QOS_1,
(int)MQTT_PACKET_FLAGS_GET_QOS(tx_buf[0]));
ASSERT_TRUE((MQTT_PACKET_FLAGS_GET(tx_buf[0]) &
MQTT_PACKET_FLAG_RETAIN) != 0);
ASSERT_TRUE((MQTT_PACKET_FLAGS_GET(tx_buf[0]) &
MQTT_PACKET_FLAG_DUPLICATE) == 0);
}

TEST(encode_publish_qos2_duplicate_flags_in_header)
{
byte tx_buf[256];
MqttPublish pub;
int rc;

XMEMSET(&pub, 0, sizeof(pub));
pub.topic_name = "test/topic";
pub.qos = MQTT_QOS_2;
pub.retain = 0;
pub.duplicate = 1;
pub.packet_id = 1;
rc = MqttEncode_Publish(tx_buf, (int)sizeof(tx_buf), &pub, 0);
ASSERT_TRUE(rc > 0);
ASSERT_EQ(MQTT_PACKET_TYPE_PUBLISH,
MQTT_PACKET_TYPE_GET(tx_buf[0]));
ASSERT_EQ(MQTT_QOS_2,
(int)MQTT_PACKET_FLAGS_GET_QOS(tx_buf[0]));
ASSERT_TRUE((MQTT_PACKET_FLAGS_GET(tx_buf[0]) &
MQTT_PACKET_FLAG_DUPLICATE) != 0);
ASSERT_TRUE((MQTT_PACKET_FLAGS_GET(tx_buf[0]) &
MQTT_PACKET_FLAG_RETAIN) == 0);
}

/* QoS 0, no retain, no dup -> flag nibble must be 0. Catches any mutation
* that unconditionally sets flag bits. */
TEST(encode_publish_qos0_no_flags_in_header)
{
byte tx_buf[256];
MqttPublish pub;
int rc;

XMEMSET(&pub, 0, sizeof(pub));
pub.topic_name = "test/topic";
pub.qos = MQTT_QOS_0;
pub.retain = 0;
pub.duplicate = 0;
rc = MqttEncode_Publish(tx_buf, (int)sizeof(tx_buf), &pub, 0);
ASSERT_TRUE(rc > 0);
ASSERT_EQ(MQTT_PACKET_TYPE_PUBLISH,
MQTT_PACKET_TYPE_GET(tx_buf[0]));
ASSERT_EQ(0, (int)MQTT_PACKET_FLAGS_GET(tx_buf[0]));
}

/* ============================================================================
* MqttDecode_Publish
* ============================================================================ */

TEST(decode_publish_qos0_valid)
{
/* Fixed header (PUBLISH, QoS 0, remain_len=7), topic "a/b",
* payload "HI". Using nonzero payload bytes catches a
* qos>MQTT_QOS_0 -> qos>=MQTT_QOS_0 mutation that would read
* the first 2 payload bytes as a spurious packet_id. */
byte buf[] = { 0x30, 7,
0x00, 0x03, 'a', '/', 'b',
'H', 'I' };
MqttPublish pub;
int rc;

XMEMSET(&pub, 0, sizeof(pub));
rc = MqttDecode_Publish(buf, (int)sizeof(buf), &pub);
ASSERT_TRUE(rc > 0);
ASSERT_EQ(MQTT_QOS_0, pub.qos);
ASSERT_EQ(0, pub.packet_id);
ASSERT_EQ(3, pub.topic_name_len);
ASSERT_EQ(0, XMEMCMP(pub.topic_name, "a/b", 3));
ASSERT_EQ(2, (int)pub.total_len);
ASSERT_EQ(2, (int)pub.buffer_len);
ASSERT_EQ('H', pub.buffer[0]);
ASSERT_EQ('I', pub.buffer[1]);
}

TEST(decode_publish_qos1_valid)
{
/* Fixed header (PUBLISH | QoS 1 = 0x32, remain_len=7),
* topic "t", packet_id=42, payload "xy". */
byte buf[] = { 0x32, 7,
0x00, 0x01, 't',
0x00, 0x2A,
'x', 'y' };
MqttPublish pub;
int rc;

XMEMSET(&pub, 0, sizeof(pub));
rc = MqttDecode_Publish(buf, (int)sizeof(buf), &pub);
ASSERT_TRUE(rc > 0);
ASSERT_EQ(MQTT_QOS_1, pub.qos);
ASSERT_EQ(42, pub.packet_id);
ASSERT_EQ(1, pub.topic_name_len);
ASSERT_EQ(0, XMEMCMP(pub.topic_name, "t", 1));
ASSERT_EQ(2, (int)pub.total_len);
ASSERT_EQ(2, (int)pub.buffer_len);
ASSERT_EQ('x', pub.buffer[0]);
ASSERT_EQ('y', pub.buffer[1]);
}

Comment thread
embhorn marked this conversation as resolved.
/* Zero-payload PUBLISH is valid per spec; catches a
* variable_len>remain_len -> variable_len>=remain_len mutation. */
TEST(decode_publish_qos0_zero_payload)
{
byte buf[] = { 0x30, 3,
0x00, 0x01, 'a' };
MqttPublish pub;
int rc;

XMEMSET(&pub, 0, sizeof(pub));
rc = MqttDecode_Publish(buf, (int)sizeof(buf), &pub);
ASSERT_TRUE(rc > 0);
ASSERT_EQ(MQTT_QOS_0, pub.qos);
ASSERT_EQ(1, pub.topic_name_len);
ASSERT_EQ(0, (int)pub.total_len);
ASSERT_EQ(0, (int)pub.buffer_len);
}

/* Fixed header claims remain_len=3, but topic declares length=5
* (consuming 7 bytes of variable header). After decoding the topic,
* variable_len (7) exceeds remain_len (3), which must be rejected. */
TEST(decode_publish_malformed_variable_exceeds_remain)
{
byte buf[] = { 0x30, 3,
0x00, 0x05, 'h', 'e', 'l', 'l', 'o' };
MqttPublish pub;
int rc;

XMEMSET(&pub, 0, sizeof(pub));
rc = MqttDecode_Publish(buf, (int)sizeof(buf), &pub);
ASSERT_EQ(MQTT_CODE_ERROR_OUT_OF_BUFFER, rc);
}

#ifdef WOLFMQTT_V5
TEST(decode_publish_v5_with_props_roundtrip)
{
byte buf[256];
byte payload[] = { 'p', 'a', 'y' };
MqttPublish enc, dec;
MqttProp prop;
char content_type[] = "text/plain";
int enc_len, dec_len;

XMEMSET(&enc, 0, sizeof(enc));
XMEMSET(&prop, 0, sizeof(prop));
prop.type = MQTT_PROP_CONTENT_TYPE;
prop.data_str.str = content_type;
prop.data_str.len = (word16)XSTRLEN(content_type);
prop.next = NULL;
enc.topic_name = "v5/topic";
enc.qos = MQTT_QOS_1;
enc.packet_id = 7;
enc.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5;
enc.props = ∝
enc.buffer = payload;
enc.total_len = sizeof(payload);

enc_len = MqttEncode_Publish(buf, (int)sizeof(buf), &enc, 0);
ASSERT_TRUE(enc_len > 0);

XMEMSET(&dec, 0, sizeof(dec));
dec.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5;
dec_len = MqttDecode_Publish(buf, enc_len, &dec);
ASSERT_TRUE(dec_len > 0);
ASSERT_EQ(MQTT_QOS_1, dec.qos);
ASSERT_EQ(7, dec.packet_id);
ASSERT_EQ((int)XSTRLEN("v5/topic"), (int)dec.topic_name_len);
ASSERT_EQ(0, XMEMCMP(dec.topic_name, "v5/topic",
XSTRLEN("v5/topic")));
ASSERT_EQ((int)sizeof(payload), (int)dec.total_len);
ASSERT_TRUE(dec.props != NULL);
if (dec.props) {
MqttProps_Free(dec.props);
}
}
#endif /* WOLFMQTT_V5 */

/* ============================================================================
* MqttDecode_ConnectAck
* ============================================================================ */
Expand Down Expand Up @@ -429,6 +630,29 @@ TEST(encode_subscribe_valid)
ASSERT_TRUE(rc > 0);
}

/* [MQTT-3.8.1-1] SUBSCRIBE fixed header flags must be 0b0010 (reserved).
* Verifies the QoS branch in MqttEncode_FixedHeader fires even for non-
* PUBLISH packets. */
TEST(encode_subscribe_fixed_header_flags)
{
byte tx_buf[256];
MqttSubscribe sub;
MqttTopic topic;
int rc;

XMEMSET(&sub, 0, sizeof(sub));
XMEMSET(&topic, 0, sizeof(topic));
topic.topic_filter = "test/topic";
sub.topics = &topic;
sub.topic_count = 1;
sub.packet_id = 1;
rc = MqttEncode_Subscribe(tx_buf, (int)sizeof(tx_buf), &sub);
ASSERT_TRUE(rc > 0);
ASSERT_EQ(MQTT_PACKET_TYPE_SUBSCRIBE,
MQTT_PACKET_TYPE_GET(tx_buf[0]));
ASSERT_EQ(0x2, (int)MQTT_PACKET_FLAGS_GET(tx_buf[0]));
}

/* ============================================================================
* MqttEncode_Unsubscribe
* ============================================================================ */
Expand Down Expand Up @@ -467,6 +691,27 @@ TEST(encode_unsubscribe_valid)
ASSERT_TRUE(rc > 0);
}

/* [MQTT-3.10.1-1] UNSUBSCRIBE fixed header flags must be 0b0010. */
TEST(encode_unsubscribe_fixed_header_flags)
{
byte tx_buf[256];
MqttUnsubscribe unsub;
MqttTopic topic;
int rc;

XMEMSET(&unsub, 0, sizeof(unsub));
XMEMSET(&topic, 0, sizeof(topic));
topic.topic_filter = "test/topic";
unsub.topics = &topic;
unsub.topic_count = 1;
unsub.packet_id = 1;
rc = MqttEncode_Unsubscribe(tx_buf, (int)sizeof(tx_buf), &unsub);
ASSERT_TRUE(rc > 0);
ASSERT_EQ(MQTT_PACKET_TYPE_UNSUBSCRIBE,
MQTT_PACKET_TYPE_GET(tx_buf[0]));
ASSERT_EQ(0x2, (int)MQTT_PACKET_FLAGS_GET(tx_buf[0]));
}

/* ============================================================================
* MqttEncode_Connect
* ============================================================================ */
Expand Down Expand Up @@ -528,6 +773,22 @@ TEST(encode_connect_no_credentials)
ASSERT_TRUE(rc > 0);
}

/* [MQTT-3.1.1] CONNECT fixed header flags must be all zero. */
TEST(encode_connect_fixed_header_flags)
{
byte tx_buf[256];
MqttConnect conn;
int rc;

XMEMSET(&conn, 0, sizeof(conn));
conn.client_id = "test_client";
rc = MqttEncode_Connect(tx_buf, (int)sizeof(tx_buf), &conn);
ASSERT_TRUE(rc > 0);
ASSERT_EQ(MQTT_PACKET_TYPE_CONNECT,
MQTT_PACKET_TYPE_GET(tx_buf[0]));
ASSERT_EQ(0, (int)MQTT_PACKET_FLAGS_GET(tx_buf[0]));
}

/* ============================================================================
* QoS 2 next-ack arithmetic (PUBLISH_REC -> REL -> COMP)
* ============================================================================ */
Expand Down Expand Up @@ -822,6 +1083,18 @@ 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_qos1_retain_flags_in_header);
RUN_TEST(encode_publish_qos2_duplicate_flags_in_header);
RUN_TEST(encode_publish_qos0_no_flags_in_header);

/* MqttDecode_Publish */
RUN_TEST(decode_publish_qos0_valid);
RUN_TEST(decode_publish_qos1_valid);
RUN_TEST(decode_publish_qos0_zero_payload);
RUN_TEST(decode_publish_malformed_variable_exceeds_remain);
#ifdef WOLFMQTT_V5
RUN_TEST(decode_publish_v5_with_props_roundtrip);
#endif

/* MqttDecode_ConnectAck */
RUN_TEST(decode_connack_valid);
Expand All @@ -831,16 +1104,19 @@ void run_mqtt_packet_tests(void)
/* MqttEncode_Subscribe */
RUN_TEST(encode_subscribe_packet_id_zero);
RUN_TEST(encode_subscribe_valid);
RUN_TEST(encode_subscribe_fixed_header_flags);

/* MqttEncode_Unsubscribe */
RUN_TEST(encode_unsubscribe_packet_id_zero);
RUN_TEST(encode_unsubscribe_valid);
RUN_TEST(encode_unsubscribe_fixed_header_flags);

/* MqttEncode_Connect */
RUN_TEST(encode_connect_password_without_username);
RUN_TEST(encode_connect_username_and_password);
RUN_TEST(encode_connect_username_only);
RUN_TEST(encode_connect_no_credentials);
RUN_TEST(encode_connect_fixed_header_flags);

/* QoS 2 ack arithmetic */
RUN_TEST(qos2_ack_arithmetic);
Expand Down
Loading