Line data Source code
1 : /**
2 : * @file test_rpc.c
3 : * @brief Unit tests for MTProto RPC framework.
4 : *
5 : * Uses mock socket + mock crypto to verify message framing.
6 : */
7 :
8 : #include "test_helpers.h"
9 : #include "mtproto_rpc.h"
10 : #include "mtproto_session.h"
11 : #include "tl_serial.h"
12 : #include "mock_socket.h"
13 : #include "mock_crypto.h"
14 : #include "transport.h"
15 :
16 : #include <stdlib.h>
17 : #include <string.h>
18 : #include <stdio.h>
19 :
20 1 : void test_rpc_send_unencrypted_framing(void) {
21 1 : mock_socket_reset();
22 1 : mock_crypto_reset();
23 : MtProtoSession s;
24 1 : mtproto_session_init(&s);
25 :
26 : Transport t;
27 1 : transport_init(&t);
28 1 : transport_connect(&t, "localhost", 443);
29 :
30 : /* Send a 4-byte payload */
31 1 : uint8_t payload[4] = {0x01, 0x02, 0x03, 0x04};
32 1 : int rc = rpc_send_unencrypted(&s, &t, payload, 4);
33 1 : ASSERT(rc == 0, "send_unencrypted should succeed");
34 :
35 : /* Verify sent data */
36 1 : size_t sent_len = 0;
37 1 : (void)mock_socket_get_sent(&sent_len);
38 : /* sent_len includes the 0xEF abridged marker (1) + abridged prefix + payload */
39 : /* The abridged encoding wraps the RPC frame */
40 1 : ASSERT(sent_len > 20, "should have sent more than 20 bytes");
41 :
42 1 : transport_close(&t);
43 : }
44 :
45 1 : void test_rpc_recv_unencrypted(void) {
46 1 : mock_socket_reset();
47 1 : mock_crypto_reset();
48 : MtProtoSession s;
49 1 : mtproto_session_init(&s);
50 :
51 : Transport t;
52 1 : transport_init(&t);
53 1 : transport_connect(&t, "localhost", 443);
54 :
55 : /* Build a response: auth_key_id(8)=0 + msg_id(8)=99 + len(4)=4 + data(4) */
56 : uint8_t response[24];
57 1 : memset(response, 0, sizeof(response));
58 : /* auth_key_id = 0 (bytes 0-7) */
59 : /* msg_id = 99 at byte 8 */
60 1 : uint64_t msg_id = 99;
61 1 : memcpy(response + 8, &msg_id, 8);
62 : /* len = 4 at byte 16 */
63 1 : uint32_t data_len = 4;
64 1 : memcpy(response + 16, &data_len, 4);
65 : /* data at byte 20 */
66 1 : response[20] = 0xAA;
67 1 : response[21] = 0xBB;
68 1 : response[22] = 0xCC;
69 1 : response[23] = 0xDD;
70 :
71 : /* Abridged encode: length in 4-byte units = 24/4 = 6, fits in 1 byte */
72 : uint8_t wire[25];
73 1 : wire[0] = 6; /* abridged length prefix */
74 1 : memcpy(wire + 1, response, 24);
75 1 : mock_socket_set_response(wire, 25);
76 :
77 : /* Clear sent data (abridged marker) */
78 1 : mock_socket_clear_sent();
79 :
80 : /* Now receive */
81 : uint8_t out[64];
82 1 : size_t out_len = 0;
83 1 : int rc = rpc_recv_unencrypted(&s, &t, out, sizeof(out), &out_len);
84 1 : ASSERT(rc == 0, "recv_unencrypted should succeed");
85 1 : ASSERT(out_len == 4, "payload length should be 4");
86 1 : ASSERT(out[0] == 0xAA, "payload byte 0");
87 1 : ASSERT(out[1] == 0xBB, "payload byte 1");
88 :
89 1 : transport_close(&t);
90 : }
91 :
92 1 : void test_rpc_send_unencrypted_null_checks(void) {
93 : MtProtoSession s;
94 1 : mtproto_session_init(&s);
95 1 : uint8_t data[4] = {0};
96 :
97 1 : ASSERT(rpc_send_unencrypted(NULL, NULL, data, 4) == -1,
98 : "NULL session should fail");
99 1 : ASSERT(rpc_send_unencrypted(&s, NULL, data, 4) == -1,
100 : "NULL transport should fail");
101 1 : ASSERT(rpc_send_unencrypted(&s, (Transport*)(intptr_t)1, NULL, 4) == -1,
102 : "NULL data should fail");
103 : }
104 :
105 1 : void test_rpc_recv_unencrypted_short_packet(void) {
106 1 : mock_socket_reset();
107 1 : mock_crypto_reset();
108 : MtProtoSession s;
109 1 : mtproto_session_init(&s);
110 :
111 : Transport t;
112 1 : transport_init(&t);
113 1 : transport_connect(&t, "localhost", 443);
114 1 : mock_socket_clear_sent();
115 :
116 : /* Too-short packet (10 bytes) */
117 : uint8_t wire[11];
118 1 : wire[0] = 3; /* abridged: 3*4=12 bytes, but we only provide 10 */
119 1 : uint8_t short_data[10] = {0};
120 1 : memcpy(wire + 1, short_data, 10);
121 1 : mock_socket_set_response(wire, 11);
122 :
123 : uint8_t out[64];
124 1 : size_t out_len = 0;
125 1 : int rc = rpc_recv_unencrypted(&s, &t, out, sizeof(out), &out_len);
126 1 : ASSERT(rc == -1, "short packet should fail");
127 :
128 1 : transport_close(&t);
129 : }
130 :
131 : /* ---- msg_container tests ---- */
132 :
133 1 : void test_container_not_container(void) {
134 : /* Non-container data → single message */
135 1 : uint8_t data[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 };
136 : RpcContainerMsg msgs[4];
137 1 : size_t count = 0;
138 :
139 1 : int rc = rpc_parse_container(data, sizeof(data), msgs, 4, &count);
140 1 : ASSERT(rc == 0, "non-container should succeed");
141 1 : ASSERT(count == 1, "count should be 1");
142 1 : ASSERT(msgs[0].body_len == sizeof(data), "body_len should match");
143 1 : ASSERT(msgs[0].body == data, "body should point to original data");
144 : }
145 :
146 1 : void test_container_single_msg(void) {
147 : /* Container with 1 message */
148 : TlWriter w;
149 1 : tl_writer_init(&w);
150 1 : tl_write_uint32(&w, 0x73f1f8dc); /* msg_container */
151 1 : tl_write_uint32(&w, 1); /* count = 1 */
152 : /* msg: msg_id(8) + seqno(4) + body_len(4) + body */
153 1 : tl_write_uint64(&w, 12345); /* msg_id */
154 1 : tl_write_uint32(&w, 1); /* seqno */
155 1 : tl_write_uint32(&w, 4); /* body_len */
156 1 : uint8_t body[] = { 0xAA, 0xBB, 0xCC, 0xDD };
157 1 : tl_write_raw(&w, body, 4);
158 :
159 : RpcContainerMsg msgs[4];
160 1 : size_t count = 0;
161 1 : int rc = rpc_parse_container(w.data, w.len, msgs, 4, &count);
162 1 : ASSERT(rc == 0, "single-msg container should succeed");
163 1 : ASSERT(count == 1, "count should be 1");
164 1 : ASSERT(msgs[0].msg_id == 12345, "msg_id should be 12345");
165 1 : ASSERT(msgs[0].seqno == 1, "seqno should be 1");
166 1 : ASSERT(msgs[0].body_len == 4, "body_len should be 4");
167 1 : ASSERT(memcmp(msgs[0].body, body, 4) == 0, "body should match");
168 :
169 1 : tl_writer_free(&w);
170 : }
171 :
172 1 : void test_container_multiple_msgs(void) {
173 : /* Container with 3 messages */
174 : TlWriter w;
175 1 : tl_writer_init(&w);
176 1 : tl_write_uint32(&w, 0x73f1f8dc);
177 1 : tl_write_uint32(&w, 3);
178 :
179 4 : for (int i = 0; i < 3; i++) {
180 3 : tl_write_uint64(&w, (uint64_t)(100 + i)); /* msg_id */
181 3 : tl_write_uint32(&w, (uint32_t)(i * 2)); /* seqno */
182 3 : tl_write_uint32(&w, 4); /* body_len */
183 3 : uint8_t body[4] = { (uint8_t)i, 0, 0, 0 };
184 3 : tl_write_raw(&w, body, 4);
185 : }
186 :
187 : RpcContainerMsg msgs[8];
188 1 : size_t count = 0;
189 1 : int rc = rpc_parse_container(w.data, w.len, msgs, 8, &count);
190 1 : ASSERT(rc == 0, "multi-msg container should succeed");
191 1 : ASSERT(count == 3, "count should be 3");
192 1 : ASSERT(msgs[0].msg_id == 100, "msg 0 id");
193 1 : ASSERT(msgs[1].msg_id == 101, "msg 1 id");
194 1 : ASSERT(msgs[2].msg_id == 102, "msg 2 id");
195 1 : ASSERT(msgs[0].body[0] == 0, "msg 0 body");
196 1 : ASSERT(msgs[1].body[0] == 1, "msg 1 body");
197 1 : ASSERT(msgs[2].body[0] == 2, "msg 2 body");
198 :
199 1 : tl_writer_free(&w);
200 : }
201 :
202 1 : void test_container_too_many_msgs(void) {
203 : /* Container with more messages than buffer can hold */
204 : TlWriter w;
205 1 : tl_writer_init(&w);
206 1 : tl_write_uint32(&w, 0x73f1f8dc);
207 1 : tl_write_uint32(&w, 5); /* 5 messages */
208 6 : for (int i = 0; i < 5; i++) {
209 5 : tl_write_uint64(&w, (uint64_t)i);
210 5 : tl_write_uint32(&w, 0);
211 5 : tl_write_uint32(&w, 4);
212 5 : uint8_t body[4] = {0};
213 5 : tl_write_raw(&w, body, 4);
214 : }
215 :
216 : RpcContainerMsg msgs[2]; /* only room for 2 */
217 1 : size_t count = 0;
218 1 : int rc = rpc_parse_container(w.data, w.len, msgs, 2, &count);
219 1 : ASSERT(rc == -1, "should fail when too many messages for buffer");
220 1 : tl_writer_free(&w);
221 : }
222 :
223 1 : void test_container_null_args(void) {
224 1 : uint8_t data[8] = {0};
225 : RpcContainerMsg msgs[2];
226 1 : size_t count = 0;
227 1 : ASSERT(rpc_parse_container(NULL, 8, msgs, 2, &count) == -1, "NULL data");
228 1 : ASSERT(rpc_parse_container(data, 8, NULL, 2, &count) == -1, "NULL msgs");
229 1 : ASSERT(rpc_parse_container(data, 8, msgs, 2, NULL) == -1, "NULL count");
230 : }
231 :
232 1 : void test_container_unaligned_body_len(void) {
233 : /* Regression: QA-20 — a container with body_len that is not a multiple
234 : * of 4 must be rejected to prevent silent misalignment of subsequent
235 : * message reads. */
236 : TlWriter w;
237 1 : tl_writer_init(&w);
238 1 : tl_write_uint32(&w, 0x73f1f8dc); /* msg_container */
239 1 : tl_write_uint32(&w, 1); /* count = 1 */
240 1 : tl_write_uint64(&w, 12345); /* msg_id */
241 1 : tl_write_uint32(&w, 1); /* seqno */
242 1 : tl_write_uint32(&w, 3); /* body_len = 3 (odd, not 4-aligned) */
243 1 : uint8_t body[4] = { 0xAA, 0xBB, 0xCC, 0x00 };
244 1 : tl_write_raw(&w, body, 4);
245 :
246 : RpcContainerMsg msgs[4];
247 1 : size_t count = 0;
248 1 : int rc = rpc_parse_container(w.data, w.len, msgs, 4, &count);
249 1 : ASSERT(rc == -1, "container with unaligned body_len must be rejected");
250 :
251 1 : tl_writer_free(&w);
252 : }
253 :
254 : /* ---- rpc_result / rpc_error tests ---- */
255 :
256 1 : void test_rpc_unwrap_result(void) {
257 : TlWriter w;
258 1 : tl_writer_init(&w);
259 1 : tl_write_uint32(&w, 0xf35c6d01); /* rpc_result */
260 1 : tl_write_uint64(&w, 99887766ULL); /* req_msg_id */
261 1 : tl_write_uint32(&w, 0xDEADBEEF); /* inner data (some constructor) */
262 1 : tl_write_int32(&w, 42);
263 :
264 1 : uint64_t req_id = 0;
265 1 : const uint8_t *inner = NULL;
266 1 : size_t inner_len = 0;
267 1 : int rc = rpc_unwrap_result(w.data, w.len, &req_id, &inner, &inner_len);
268 1 : ASSERT(rc == 0, "unwrap rpc_result should succeed");
269 1 : ASSERT(req_id == 99887766ULL, "req_msg_id should match");
270 1 : ASSERT(inner_len == 8, "inner should be 8 bytes (constructor + int32)");
271 :
272 : uint32_t inner_crc;
273 1 : memcpy(&inner_crc, inner, 4);
274 1 : ASSERT(inner_crc == 0xDEADBEEF, "inner constructor should match");
275 :
276 1 : tl_writer_free(&w);
277 : }
278 :
279 1 : void test_rpc_unwrap_result_not_result(void) {
280 : TlWriter w;
281 1 : tl_writer_init(&w);
282 1 : tl_write_uint32(&w, 0x12345678); /* not rpc_result */
283 1 : tl_write_int32(&w, 42);
284 :
285 : uint64_t req_id;
286 : const uint8_t *inner;
287 : size_t inner_len;
288 1 : int rc = rpc_unwrap_result(w.data, w.len, &req_id, &inner, &inner_len);
289 1 : ASSERT(rc == -1, "non-rpc_result should return -1");
290 1 : tl_writer_free(&w);
291 : }
292 :
293 1 : void test_rpc_parse_error_flood_wait(void) {
294 : TlWriter w;
295 1 : tl_writer_init(&w);
296 1 : tl_write_uint32(&w, 0x2144ca19); /* rpc_error */
297 1 : tl_write_int32(&w, 420); /* error_code */
298 1 : tl_write_string(&w, "FLOOD_WAIT_30");
299 :
300 : RpcError err;
301 1 : int rc = rpc_parse_error(w.data, w.len, &err);
302 1 : ASSERT(rc == 0, "parse flood_wait should succeed");
303 1 : ASSERT(err.error_code == 420, "error_code should be 420");
304 1 : ASSERT(strcmp(err.error_msg, "FLOOD_WAIT_30") == 0, "error_msg should match");
305 1 : ASSERT(err.flood_wait_secs == 30, "flood_wait should be 30 seconds");
306 1 : ASSERT(err.migrate_dc == -1, "no migration");
307 :
308 1 : tl_writer_free(&w);
309 : }
310 :
311 1 : void test_rpc_parse_error_phone_migrate(void) {
312 : TlWriter w;
313 1 : tl_writer_init(&w);
314 1 : tl_write_uint32(&w, 0x2144ca19);
315 1 : tl_write_int32(&w, 303);
316 1 : tl_write_string(&w, "PHONE_MIGRATE_4");
317 :
318 : RpcError err;
319 1 : int rc = rpc_parse_error(w.data, w.len, &err);
320 1 : ASSERT(rc == 0, "parse phone_migrate should succeed");
321 1 : ASSERT(err.error_code == 303, "error_code should be 303");
322 1 : ASSERT(err.migrate_dc == 4, "should migrate to DC 4");
323 1 : ASSERT(err.flood_wait_secs == 0, "no flood wait");
324 :
325 1 : tl_writer_free(&w);
326 : }
327 :
328 1 : void test_rpc_parse_error_file_migrate(void) {
329 : TlWriter w;
330 1 : tl_writer_init(&w);
331 1 : tl_write_uint32(&w, 0x2144ca19);
332 1 : tl_write_int32(&w, 303);
333 1 : tl_write_string(&w, "FILE_MIGRATE_2");
334 :
335 : RpcError err;
336 1 : int rc = rpc_parse_error(w.data, w.len, &err);
337 1 : ASSERT(rc == 0, "parse file_migrate should succeed");
338 1 : ASSERT(err.migrate_dc == 2, "should migrate to DC 2");
339 :
340 1 : tl_writer_free(&w);
341 : }
342 :
343 1 : void test_rpc_parse_error_session_password(void) {
344 : TlWriter w;
345 1 : tl_writer_init(&w);
346 1 : tl_write_uint32(&w, 0x2144ca19);
347 1 : tl_write_int32(&w, 401);
348 1 : tl_write_string(&w, "SESSION_PASSWORD_NEEDED");
349 :
350 : RpcError err;
351 1 : int rc = rpc_parse_error(w.data, w.len, &err);
352 1 : ASSERT(rc == 0, "parse session_password should succeed");
353 1 : ASSERT(err.error_code == 401, "error_code should be 401");
354 1 : ASSERT(strcmp(err.error_msg, "SESSION_PASSWORD_NEEDED") == 0, "msg");
355 1 : ASSERT(err.migrate_dc == -1, "no migration");
356 1 : ASSERT(err.flood_wait_secs == 0, "no flood wait");
357 :
358 1 : tl_writer_free(&w);
359 : }
360 :
361 1 : void test_rpc_parse_error_not_error(void) {
362 : TlWriter w;
363 1 : tl_writer_init(&w);
364 1 : tl_write_uint32(&w, 0x12345678); /* not rpc_error */
365 1 : tl_write_int32(&w, 200);
366 :
367 : RpcError err;
368 1 : int rc = rpc_parse_error(w.data, w.len, &err);
369 1 : ASSERT(rc == -1, "non-rpc_error should return -1");
370 :
371 1 : tl_writer_free(&w);
372 : }
373 :
374 1 : void test_rpc_parse_error_null_args(void) {
375 1 : uint8_t data[16] = {0};
376 : RpcError err;
377 1 : ASSERT(rpc_parse_error(NULL, 16, &err) == -1, "NULL data");
378 1 : ASSERT(rpc_parse_error(data, 16, NULL) == -1, "NULL err");
379 : }
380 :
381 : /* ---- rpc_recv_encrypted validation tests ---- */
382 :
383 : /**
384 : * Build a minimal valid encrypted wire frame and put it in the mock socket.
385 : *
386 : * With mock crypto:
387 : * - SHA256 always returns 32 zero bytes, so auth_key_id = 0 and msg_key = 0.
388 : * - AES decrypt is an identity transform, so decrypted == ciphertext.
389 : * - msg_key verification inside mtproto_decrypt passes when msg_key == 0.
390 : *
391 : * The plaintext layout:
392 : * salt(8) | session_id(8) | msg_id(8) | seq_no(4) | data_len(4) | data(4) | pad(16)
393 : * = 52 bytes total → round to 64 (multiple of 16 for AES).
394 : *
395 : * @param session_id_override Value to write into the session_id field of the frame.
396 : * @param auth_key_id_override Value to write into the outer auth_key_id field.
397 : * @param local_session_id Session's actual session_id.
398 : */
399 2 : static void build_encrypted_frame(uint64_t auth_key_id_override,
400 : uint64_t session_id_in_frame,
401 : Transport *t) {
402 : /* Plaintext: must be multiple of 16. We use 64 bytes. */
403 : uint8_t plain[64];
404 2 : memset(plain, 0, sizeof(plain));
405 :
406 2 : uint64_t salt = 0;
407 2 : memcpy(plain + 0, &salt, 8); /* salt */
408 2 : memcpy(plain + 8, &session_id_in_frame, 8); /* session_id */
409 : /* msg_id, seq_no, data_len, data, padding stay zero */
410 :
411 : /* With mock crypto, AES-IGE decrypt is identity: cipher == plain. */
412 : uint8_t cipher[64];
413 2 : memcpy(cipher, plain, 64);
414 :
415 : /* Wire frame: auth_key_id(8) + msg_key(16, zeros) + cipher(64) = 88 bytes */
416 : uint8_t frame[88];
417 2 : memcpy(frame + 0, &auth_key_id_override, 8); /* auth_key_id */
418 2 : memset(frame + 8, 0, 16); /* msg_key = zeros */
419 2 : memcpy(frame + 24, cipher, 64);
420 :
421 : /* Abridged encoding: length in 4-byte units = 88/4 = 22 → fits in 1 byte */
422 : uint8_t wire[89];
423 2 : wire[0] = 22;
424 2 : memcpy(wire + 1, frame, 88);
425 2 : mock_socket_set_response(wire, 89);
426 :
427 : (void)t;
428 2 : }
429 :
430 1 : void test_recv_encrypted_wrong_auth_key_id(void) {
431 1 : mock_socket_reset();
432 1 : mock_crypto_reset();
433 :
434 : MtProtoSession s;
435 1 : mtproto_session_init(&s);
436 1 : s.has_auth_key = 1;
437 1 : memset(s.auth_key, 0, 256);
438 : /* With mock SHA256 = zeros, the expected auth_key_id is 0.
439 : * We deliberately use a non-zero auth_key_id to trigger rejection. */
440 1 : uint64_t wrong_id = 0xDEADBEEFCAFEBABEULL;
441 :
442 : Transport t;
443 1 : transport_init(&t);
444 1 : transport_connect(&t, "localhost", 443);
445 1 : mock_socket_clear_sent();
446 :
447 1 : build_encrypted_frame(wrong_id, s.session_id, &t);
448 :
449 : uint8_t out[256];
450 1 : size_t out_len = 0;
451 1 : int rc = rpc_recv_encrypted(&s, &t, out, sizeof(out), &out_len);
452 1 : ASSERT(rc == -1, "wrong auth_key_id must be rejected");
453 :
454 1 : transport_close(&t);
455 : }
456 :
457 1 : void test_recv_encrypted_wrong_session_id(void) {
458 1 : mock_socket_reset();
459 1 : mock_crypto_reset();
460 :
461 : MtProtoSession s;
462 1 : mtproto_session_init(&s);
463 1 : s.has_auth_key = 1;
464 1 : memset(s.auth_key, 0, 256);
465 : /* Correct auth_key_id = 0 (mock SHA256 zeros); wrong session_id in frame. */
466 1 : uint64_t correct_auth_key_id = 0ULL;
467 1 : uint64_t wrong_session_id = s.session_id ^ 0xFFFFFFFFFFFFFFFFULL;
468 :
469 : Transport t;
470 1 : transport_init(&t);
471 1 : transport_connect(&t, "localhost", 443);
472 1 : mock_socket_clear_sent();
473 :
474 1 : build_encrypted_frame(correct_auth_key_id, wrong_session_id, &t);
475 :
476 : uint8_t out[256];
477 1 : size_t out_len = 0;
478 1 : int rc = rpc_recv_encrypted(&s, &t, out, sizeof(out), &out_len);
479 1 : ASSERT(rc == -1, "wrong session_id must be rejected");
480 :
481 1 : transport_close(&t);
482 : }
483 :
484 1 : void test_rpc(void) {
485 1 : RUN_TEST(test_rpc_send_unencrypted_framing);
486 1 : RUN_TEST(test_rpc_recv_unencrypted);
487 1 : RUN_TEST(test_rpc_send_unencrypted_null_checks);
488 1 : RUN_TEST(test_rpc_recv_unencrypted_short_packet);
489 :
490 : /* msg_container */
491 1 : RUN_TEST(test_container_not_container);
492 1 : RUN_TEST(test_container_single_msg);
493 1 : RUN_TEST(test_container_multiple_msgs);
494 1 : RUN_TEST(test_container_too_many_msgs);
495 1 : RUN_TEST(test_container_null_args);
496 1 : RUN_TEST(test_container_unaligned_body_len);
497 :
498 : /* rpc_recv_encrypted validation */
499 1 : RUN_TEST(test_recv_encrypted_wrong_auth_key_id);
500 1 : RUN_TEST(test_recv_encrypted_wrong_session_id);
501 :
502 : /* rpc_result / rpc_error */
503 1 : RUN_TEST(test_rpc_unwrap_result);
504 1 : RUN_TEST(test_rpc_unwrap_result_not_result);
505 1 : RUN_TEST(test_rpc_parse_error_flood_wait);
506 1 : RUN_TEST(test_rpc_parse_error_phone_migrate);
507 1 : RUN_TEST(test_rpc_parse_error_file_migrate);
508 1 : RUN_TEST(test_rpc_parse_error_session_password);
509 1 : RUN_TEST(test_rpc_parse_error_not_error);
510 1 : RUN_TEST(test_rpc_parse_error_null_args);
511 1 : }
|