LCOV - code coverage report
Current view: top level - tests/functional - test_handshake_cold_boot.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 100.0 % 158 158
Test Date: 2026-04-20 19:54:22 Functions: 100.0 % 15 15

            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 : }
        

Generated by: LCOV version 2.0-1