Line data Source code
1 : /**
2 : * @file test_handshake_cold_boot.c
3 : * @brief TEST-71 / US-20 — functional coverage for the MTProto 2.0 DH
4 : * handshake (src/infrastructure/mtproto_auth.c).
5 : *
6 : * Drives the production auth_step_* functions against the in-process
7 : * mock Telegram server with real OpenSSL on both sides. The mock
8 : * cannot decrypt the client's RSA_PAD-encrypted inner_data (that would
9 : * require Telegram's RSA private key, not shipped), so these tests
10 : * cover all paths reachable WITHOUT that private key:
11 : *
12 : * - req_pq_multi → resPQ happy path (auth_step_req_pq)
13 : * - resPQ wrong fingerprint, wrong constructor, wrong nonce, bad PQ
14 : * - auth_step_req_dh end-to-end (RSA_PAD encrypt, wire send)
15 : * - auth_step_parse_dh rejection of a garbage server_DH_params_ok
16 : * - mtproto_auth_key_gen orchestrator failure path + no partial
17 : * session persistence
18 : *
19 : * The fresh-install happy-path (session.bin created) and the
20 : * dh_gen_retry / dh_gen_fail variants are explicitly out of scope for
21 : * this test suite because they require the mock to fabricate a valid
22 : * AES-IGE-wrapped server_DH_inner_data, which in turn requires knowing
23 : * the client's new_nonce — sealed inside the RSA envelope. Those
24 : * scenarios remain covered at unit-test granularity in tests/unit/
25 : * test_auth.c (which uses the mock crypto backend).
26 : */
27 :
28 : #include "test_helpers.h"
29 :
30 : #include "mock_socket.h"
31 : #include "mock_tel_server.h"
32 :
33 : #include "mtproto_auth.h"
34 : #include "mtproto_session.h"
35 : #include "transport.h"
36 : #include "app/session_store.h"
37 :
38 : #include <stdio.h>
39 : #include <stdlib.h>
40 : #include <string.h>
41 : #include <sys/stat.h>
42 : #include <unistd.h>
43 :
44 : #define CRC_req_pq_multi 0xbe7e8ef1U
45 : #define CRC_req_DH_params 0xd712e4beU
46 :
47 : /* ---- Helpers ---------------------------------------------------------- */
48 :
49 20 : static void with_tmp_home(const char *tag) {
50 : char tmp[256];
51 20 : snprintf(tmp, sizeof(tmp), "/tmp/tg-cli-handshake-%s", tag);
52 : /* Best-effort cleanup of any previous session.bin from an earlier run. */
53 : char bin[512];
54 20 : snprintf(bin, sizeof(bin), "%s/.config/tg-cli/session.bin", tmp);
55 20 : (void)unlink(bin);
56 20 : setenv("HOME", tmp, 1);
57 20 : }
58 :
59 10 : static int session_bin_exists(void) {
60 10 : const char *home = getenv("HOME");
61 10 : if (!home) return 0;
62 : char path[512];
63 10 : snprintf(path, sizeof(path), "%s/.config/tg-cli/session.bin", home);
64 : struct stat st;
65 10 : return stat(path, &st) == 0 ? 1 : 0;
66 : }
67 :
68 20 : static void fresh_mock(const char *tag) {
69 20 : with_tmp_home(tag);
70 20 : mt_server_init();
71 20 : mt_server_reset();
72 20 : }
73 :
74 20 : static void bring_transport_up(Transport *t, MtProtoSession *s) {
75 20 : transport_init(t);
76 20 : ASSERT(transport_connect(t, "127.0.0.1", 443) == 0, "transport_connect");
77 20 : mtproto_session_init(s);
78 : }
79 :
80 : /* ---- Step 1: req_pq_multi → resPQ ----------------------------------- */
81 :
82 : /**
83 : * Happy-path: with a valid resPQ armed, auth_step_req_pq should
84 : * succeed, populate ctx.pq / ctx.server_nonce, and the mock should
85 : * have observed exactly one req_pq_multi frame.
86 : */
87 2 : static void test_cold_boot_req_pq_happy_path(void) {
88 2 : fresh_mock("req-pq-happy");
89 2 : mt_server_simulate_cold_boot(MT_COLD_BOOT_OK);
90 :
91 : Transport t; MtProtoSession s;
92 2 : bring_transport_up(&t, &s);
93 :
94 2 : AuthKeyCtx ctx; memset(&ctx, 0, sizeof(ctx));
95 2 : ctx.transport = &t;
96 2 : ctx.session = &s;
97 2 : ctx.dc_id = 2;
98 :
99 2 : int rc = auth_step_req_pq(&ctx);
100 2 : ASSERT(rc == 0, "auth_step_req_pq returns 0 on valid resPQ");
101 2 : ASSERT(ctx.pq == 21, "ctx.pq matches the 21 the mock emits");
102 2 : ASSERT(mt_server_handshake_req_pq_count() == 1,
103 : "mock observed exactly one req_pq_multi");
104 : /* server_nonce must have been populated (0xBB fill from the mock). */
105 2 : ASSERT(ctx.server_nonce[0] == 0xBB, "ctx.server_nonce bytes from mock");
106 :
107 2 : transport_close(&t);
108 : }
109 :
110 : /**
111 : * Negative: resPQ lists a fingerprint the client's hardcoded Telegram
112 : * RSA key does not match. auth_step_req_pq must return -1 without
113 : * setting pq.
114 : */
115 2 : static void test_cold_boot_bad_fingerprint(void) {
116 2 : fresh_mock("bad-fp");
117 2 : mt_server_simulate_cold_boot(MT_COLD_BOOT_BAD_FINGERPRINT);
118 :
119 : Transport t; MtProtoSession s;
120 2 : bring_transport_up(&t, &s);
121 :
122 2 : AuthKeyCtx ctx; memset(&ctx, 0, sizeof(ctx));
123 2 : ctx.transport = &t;
124 2 : ctx.session = &s;
125 :
126 2 : int rc = auth_step_req_pq(&ctx);
127 2 : ASSERT(rc == -1, "auth_step_req_pq rejects unknown fingerprint");
128 2 : ASSERT(mt_server_handshake_req_pq_count() == 1,
129 : "client still sent req_pq_multi once before rejecting resPQ");
130 :
131 2 : transport_close(&t);
132 : }
133 :
134 : /**
135 : * Negative: resPQ uses a constructor CRC the client does not expect.
136 : * auth_step_req_pq must detect the mismatch and return -1.
137 : */
138 2 : static void test_cold_boot_wrong_constructor(void) {
139 2 : fresh_mock("bad-crc");
140 2 : mt_server_simulate_cold_boot(MT_COLD_BOOT_WRONG_CONSTRUCTOR);
141 :
142 : Transport t; MtProtoSession s;
143 2 : bring_transport_up(&t, &s);
144 :
145 2 : AuthKeyCtx ctx; memset(&ctx, 0, sizeof(ctx));
146 2 : ctx.transport = &t;
147 2 : ctx.session = &s;
148 :
149 2 : int rc = auth_step_req_pq(&ctx);
150 2 : ASSERT(rc == -1, "auth_step_req_pq rejects wrong constructor");
151 :
152 2 : transport_close(&t);
153 : }
154 :
155 : /**
156 : * Negative: server echoes the nonce back tampered. Client must detect
157 : * MITM / protocol bug and refuse to proceed (returns -1).
158 : */
159 2 : static void test_cold_boot_server_nonce_mismatch_refuses(void) {
160 2 : fresh_mock("nonce-tamper");
161 2 : mt_server_simulate_cold_boot(MT_COLD_BOOT_NONCE_TAMPER);
162 :
163 : Transport t; MtProtoSession s;
164 2 : bring_transport_up(&t, &s);
165 :
166 2 : AuthKeyCtx ctx; memset(&ctx, 0, sizeof(ctx));
167 2 : ctx.transport = &t;
168 2 : ctx.session = &s;
169 :
170 2 : int rc = auth_step_req_pq(&ctx);
171 2 : ASSERT(rc == -1, "auth_step_req_pq rejects tampered nonce echo");
172 2 : ASSERT(!s.has_auth_key, "session auth_key must NOT be set on nonce tamper");
173 :
174 2 : transport_close(&t);
175 : }
176 :
177 : /* ---- Step 2: PQ factorisation + req_DH_params --------------------- */
178 :
179 : /**
180 : * After step 1 succeeds, step 2 must factorise PQ (=21 → 3 * 7),
181 : * RSA_PAD-encrypt the inner_data, and send req_DH_params. Mock
182 : * observes the handshake counter incrementing.
183 : */
184 2 : static void test_cold_boot_step2_sends_req_dh_params(void) {
185 2 : fresh_mock("step2");
186 : /* Mode 2 ensures the mock also sends a server_DH_params_ok on the
187 : * second frame, so rpc_recv_unencrypted in auth_step_parse_dh does
188 : * not hang. For this test we only care that step 2 fires. */
189 2 : mt_server_simulate_cold_boot_through_step3();
190 :
191 : Transport t; MtProtoSession s;
192 2 : bring_transport_up(&t, &s);
193 :
194 2 : AuthKeyCtx ctx; memset(&ctx, 0, sizeof(ctx));
195 2 : ctx.transport = &t;
196 2 : ctx.session = &s;
197 2 : ctx.dc_id = 2;
198 :
199 2 : ASSERT(auth_step_req_pq(&ctx) == 0, "step 1 succeeds");
200 2 : ASSERT(ctx.pq == 21, "pq = 21 as emitted");
201 :
202 2 : int rc = auth_step_req_dh(&ctx);
203 2 : ASSERT(rc == 0, "auth_step_req_dh returns 0 on wire success");
204 2 : ASSERT(ctx.p == 3, "Pollard's rho factored pq=21 → p=3");
205 2 : ASSERT(ctx.q == 7, "Pollard's rho factored pq=21 → q=7");
206 2 : ASSERT(mt_server_handshake_req_dh_count() == 1,
207 : "mock observed exactly one req_DH_params");
208 2 : ASSERT(mt_server_request_crc_count(CRC_req_DH_params) == 1,
209 : "CRC ring also records req_DH_params");
210 :
211 2 : transport_close(&t);
212 : }
213 :
214 : /**
215 : * Negative: server emits pq that cannot be factored (a 64-bit prime
216 : * just below 2^64). Step 1 succeeds (client trusts the fingerprint
217 : * check and parses pq), but step 2's pq_factorize returns -1 so the
218 : * orchestrator bails out cleanly without writing session.bin.
219 : */
220 2 : static void test_cold_boot_bad_pq_rejected_in_step2(void) {
221 2 : fresh_mock("bad-pq");
222 2 : mt_server_simulate_cold_boot(MT_COLD_BOOT_BAD_PQ);
223 :
224 : Transport t; MtProtoSession s;
225 2 : bring_transport_up(&t, &s);
226 :
227 2 : AuthKeyCtx ctx; memset(&ctx, 0, sizeof(ctx));
228 2 : ctx.transport = &t;
229 2 : ctx.session = &s;
230 2 : ctx.dc_id = 2;
231 :
232 2 : ASSERT(auth_step_req_pq(&ctx) == 0, "step 1 still succeeds with prime pq");
233 2 : ASSERT(ctx.pq == 0xFFFFFFFFFFFFFFC5ULL, "pq is the prime mock emits");
234 :
235 2 : int rc = auth_step_req_dh(&ctx);
236 2 : ASSERT(rc == -1, "step 2 rejects unfactorable PQ");
237 :
238 2 : transport_close(&t);
239 : }
240 :
241 : /* ---- Step 3: server_DH_params_ok rejection ------------------------- */
242 :
243 : /**
244 : * With steps 1 + 2 passing and a synthetic server_DH_params_ok whose
245 : * encrypted_answer is random bytes, auth_step_parse_dh must decrypt,
246 : * find a bogus inner constructor, and return -1. The session must
247 : * remain un-keyed.
248 : */
249 2 : static void test_cold_boot_parse_dh_rejects_garbage(void) {
250 2 : fresh_mock("parse-dh-garbage");
251 2 : mt_server_simulate_cold_boot_through_step3();
252 :
253 : Transport t; MtProtoSession s;
254 2 : bring_transport_up(&t, &s);
255 :
256 2 : AuthKeyCtx ctx; memset(&ctx, 0, sizeof(ctx));
257 2 : ctx.transport = &t;
258 2 : ctx.session = &s;
259 2 : ctx.dc_id = 2;
260 :
261 2 : ASSERT(auth_step_req_pq(&ctx) == 0, "step 1 ok");
262 2 : ASSERT(auth_step_req_dh(&ctx) == 0, "step 2 ok");
263 :
264 2 : int rc = auth_step_parse_dh(&ctx);
265 2 : ASSERT(rc == -1, "step 3 rejects garbage server_DH_params_ok");
266 2 : ASSERT(!s.has_auth_key, "auth_key must NOT be set on step 3 failure");
267 :
268 2 : transport_close(&t);
269 : }
270 :
271 : /* ---- Orchestrator: mtproto_auth_key_gen ---------------------------- */
272 :
273 : /**
274 : * End-to-end via the public orchestrator. Without a valid RSA private
275 : * key on the mock side the handshake cannot complete, so the
276 : * orchestrator must return -1 and the session must remain un-keyed.
277 : * Crucially, session.bin must NOT appear — a half-finished handshake
278 : * has no useful auth_key to persist and writing anything would confuse
279 : * the next cold-boot attempt.
280 : */
281 2 : static void test_cold_boot_orchestrator_fails_cleanly(void) {
282 2 : fresh_mock("orchestrator-fail");
283 2 : ASSERT(!session_bin_exists(),
284 : "session.bin absent at start of cold-boot test");
285 :
286 2 : mt_server_simulate_cold_boot_through_step3();
287 :
288 : Transport t; MtProtoSession s;
289 2 : bring_transport_up(&t, &s);
290 :
291 2 : int rc = mtproto_auth_key_gen(&t, &s);
292 2 : ASSERT(rc == -1, "mtproto_auth_key_gen returns -1 on step-3 failure");
293 2 : ASSERT(!s.has_auth_key, "no auth_key on session after failure");
294 2 : ASSERT(!session_bin_exists(),
295 : "session.bin still absent — no partial persistence");
296 2 : ASSERT(mt_server_handshake_req_pq_count() == 1,
297 : "orchestrator sent exactly one req_pq_multi");
298 2 : ASSERT(mt_server_handshake_req_dh_count() == 1,
299 : "orchestrator sent exactly one req_DH_params before failing");
300 :
301 2 : transport_close(&t);
302 : }
303 :
304 : /**
305 : * Null-argument guard: mtproto_auth_key_gen must reject NULL without
306 : * touching session.bin. This is the only branch of the orchestrator
307 : * that does not require a live mock; keep it here so functional
308 : * coverage of mtproto_auth_key_gen's error paths is complete.
309 : */
310 2 : static void test_cold_boot_orchestrator_null_args(void) {
311 2 : fresh_mock("null-args");
312 :
313 : Transport t; MtProtoSession s;
314 2 : bring_transport_up(&t, &s);
315 :
316 2 : ASSERT(mtproto_auth_key_gen(NULL, &s) == -1, "NULL transport rejected");
317 2 : ASSERT(mtproto_auth_key_gen(&t, NULL) == -1, "NULL session rejected");
318 2 : ASSERT(!session_bin_exists(), "session.bin still absent");
319 :
320 2 : transport_close(&t);
321 : }
322 :
323 : /* ---- Full DH handshake success (TEST-72) --------------------------- */
324 :
325 : /**
326 : * End-to-end full DH handshake: the mock uses the test RSA private key to
327 : * decrypt req_DH_params, derives valid server_DH_params_ok, handles
328 : * set_client_DH_params, and sends dh_gen_ok. The orchestrator must return 0,
329 : * the session must have an auth_key, and session.bin must be persisted.
330 : */
331 2 : static void test_full_dh_handshake_succeeds(void) {
332 2 : fresh_mock("full-dh");
333 2 : ASSERT(!session_bin_exists(), "session.bin absent before full DH test");
334 :
335 2 : mt_server_simulate_full_dh_handshake();
336 :
337 : Transport t; MtProtoSession s;
338 2 : bring_transport_up(&t, &s);
339 :
340 2 : int rc = mtproto_auth_key_gen(&t, &s);
341 2 : ASSERT(rc == 0, "mtproto_auth_key_gen returns 0 on full DH success");
342 2 : ASSERT(s.has_auth_key, "session has auth_key after full DH");
343 :
344 : /* auth_key must be non-trivially non-zero (DH result is large) */
345 2 : int nonzero = 0;
346 450 : for (int i = 0; i < 256; i++) {
347 450 : if (s.auth_key[i]) { nonzero = 1; break; }
348 : }
349 2 : ASSERT(nonzero, "auth_key is non-zero after full DH");
350 :
351 2 : ASSERT(session_bin_exists(),
352 : "session.bin persisted after successful DH handshake");
353 :
354 2 : ASSERT(mt_server_handshake_req_pq_count() == 1,
355 : "orchestrator sent exactly one req_pq_multi");
356 2 : ASSERT(mt_server_handshake_req_dh_count() == 1,
357 : "orchestrator sent exactly one req_DH_params");
358 2 : ASSERT(mt_server_handshake_set_client_dh_count() == 1,
359 : "mock received exactly one set_client_DH_params");
360 :
361 2 : transport_close(&t);
362 : }
363 :
364 : /* ---- Suite entry point --------------------------------------------- */
365 :
366 2 : void run_handshake_cold_boot_tests(void) {
367 2 : RUN_TEST(test_cold_boot_req_pq_happy_path);
368 2 : RUN_TEST(test_cold_boot_bad_fingerprint);
369 2 : RUN_TEST(test_cold_boot_wrong_constructor);
370 2 : RUN_TEST(test_cold_boot_server_nonce_mismatch_refuses);
371 2 : RUN_TEST(test_cold_boot_step2_sends_req_dh_params);
372 2 : RUN_TEST(test_cold_boot_bad_pq_rejected_in_step2);
373 2 : RUN_TEST(test_cold_boot_parse_dh_rejects_garbage);
374 2 : RUN_TEST(test_cold_boot_orchestrator_fails_cleanly);
375 2 : RUN_TEST(test_cold_boot_orchestrator_null_args);
376 2 : RUN_TEST(test_full_dh_handshake_succeeds);
377 2 : }
|