LCOV - code coverage report
Current view: top level - tests/functional - test_srp_roundtrip_functional.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 89.9 % 159 143
Test Date: 2026-04-20 19:54:22 Functions: 81.8 % 11 9

            Line data    Source code
       1              : /**
       2              :  * @file test_srp_roundtrip_functional.c
       3              :  * @brief End-to-end SRP verification under real OpenSSL.
       4              :  *
       5              :  * The client-side implementation lives in
       6              :  * src/infrastructure/auth_2fa.c::srp_compute and derives (A, M1) from
       7              :  * the user's password plus server parameters (g, p, B, salts). These
       8              :  * tests rebuild an independent "server side" SRP using OpenSSL BN ops
       9              :  * directly, then assert that the math invariant
      10              :  *
      11              :  *     S_client == S_server
      12              :  *
      13              :  * holds — i.e. both ends arrive at the same shared secret. That's the
      14              :  * key correctness property of SRP-6a as adapted by Telegram; if it
      15              :  * holds, the client M1 and the server's own expected M1 will match
      16              :  * too (both hash the same K = H(S)).
      17              :  *
      18              :  * Because `crypto_rand_bytes` is real in this binary we pin the
      19              :  * client's `a` (private exponent) through the new public
      20              :  * auth_2fa_srp_compute(a_priv_in, ...) entry point so the test is
      21              :  * deterministic.
      22              :  */
      23              : 
      24              : #include "test_helpers.h"
      25              : #include "infrastructure/auth_2fa.h"
      26              : #include "crypto.h"
      27              : 
      28              : #pragma GCC diagnostic push
      29              : #pragma GCC diagnostic ignored "-Wdeprecated-declarations"
      30              : #include <openssl/bn.h>
      31              : #include <openssl/sha.h>
      32              : #pragma GCC diagnostic pop
      33              : 
      34              : #include <stdint.h>
      35              : #include <string.h>
      36              : #include <stdlib.h>
      37              : 
      38              : /* MTProto uses this fixed 2048-bit safe prime. We embed it directly so
      39              :  * the test doesn't need a live server. */
      40              : static const uint8_t MTPROTO_P[256] = {
      41              :     0xC7,0x1C,0xAE,0xB9,0xC6,0xB1,0xC9,0x04,0x8E,0x6C,0x52,0x2F,0x70,0xF1,0x3F,0x73,
      42              :     0x98,0x0D,0x40,0x23,0x8E,0x3E,0x21,0xC1,0x49,0x34,0xD0,0x37,0x56,0x3D,0x93,0x0F,
      43              :     0x48,0x19,0x8A,0x0A,0xA7,0xC1,0x40,0x58,0x22,0x94,0x93,0xD2,0x25,0x30,0xF4,0xDB,
      44              :     0xFA,0x33,0x6F,0x6E,0x0A,0xC9,0x25,0x13,0x95,0x43,0xAE,0xD4,0x4C,0xCE,0x7C,0x37,
      45              :     0x20,0xFD,0x51,0xF6,0x94,0x58,0x70,0x5A,0xC6,0x8C,0xD4,0xFE,0x6B,0x6B,0x13,0xAB,
      46              :     0xDC,0x97,0x46,0x51,0x29,0x69,0x32,0x84,0x54,0xF1,0x8F,0xAF,0x8C,0x59,0x5F,0x64,
      47              :     0x24,0x77,0xFE,0x96,0xBB,0x2A,0x94,0x1D,0x5B,0xCD,0x1D,0x4A,0xC8,0xCC,0x49,0x88,
      48              :     0x07,0x08,0xFA,0x9B,0x37,0x8E,0x3C,0x4F,0x3A,0x90,0x60,0xBE,0xE6,0x7C,0xF9,0xA4,
      49              :     0xA4,0xA6,0x95,0x81,0x10,0x51,0x90,0x7E,0x16,0x27,0x53,0xB5,0x6B,0x0F,0x6B,0x41,
      50              :     0x0D,0xBA,0x74,0xD8,0xA8,0x4B,0x2A,0x14,0xB3,0x14,0x4E,0x0E,0xF1,0x28,0x47,0x54,
      51              :     0xFD,0x17,0xED,0x95,0x0D,0x59,0x65,0xB4,0xB9,0xDD,0x46,0x58,0x2D,0xB1,0x17,0x8D,
      52              :     0x16,0x9C,0x6B,0xC4,0x65,0xB0,0xD6,0xFF,0x9C,0xA3,0x92,0x8F,0xEF,0x5B,0x9A,0xE4,
      53              :     0xE4,0x18,0xFC,0x15,0xE8,0x3E,0xBE,0xA0,0xF8,0x7F,0xA9,0xFF,0x5E,0xED,0x70,0x05,
      54              :     0x0D,0xED,0x28,0x49,0xF4,0x7B,0xF9,0x59,0xD9,0x56,0x85,0x0C,0xE9,0x29,0x85,0x1F,
      55              :     0x0D,0x81,0x15,0xF6,0x35,0xB1,0x05,0xEE,0x2E,0x4E,0x15,0xD0,0x4B,0x24,0x54,0xBF,
      56              :     0x6F,0x4F,0xAD,0xF0,0x34,0xB1,0x04,0x03,0x11,0x9C,0xD8,0xE3,0xB9,0x2F,0xCC,0x5B
      57              : };
      58              : 
      59              : /* Compute PH2(password, salt1, salt2) following the Telegram spec:
      60              :  *   PH1 = SH(SH(password, salt1), salt2)
      61              :  *        where SH(data, salt) = SHA256(salt | data | salt)
      62              :  *   PH2 = SH(pbkdf2-sha512(PH1, salt1, 100000, 64), salt2) */
      63            8 : static void ph2(const char *password,
      64              :                  const uint8_t *salt1, size_t s1,
      65              :                  const uint8_t *salt2, size_t s2,
      66              :                  uint8_t out_x[32]) {
      67            8 :     size_t plen = strlen(password);
      68              :     /* SH(password, salt1) */
      69            8 :     size_t buf_cap = (s1 * 2 + plen > s2 * 2 + 64 ? s1 * 2 + plen : s2 * 2 + 64);
      70            8 :     uint8_t *buf = (uint8_t *)malloc(buf_cap);
      71              :     uint8_t inner[32];
      72            8 :     memcpy(buf, salt1, s1);
      73            8 :     memcpy(buf + s1, password, plen);
      74            8 :     memcpy(buf + s1 + plen, salt1, s1);
      75            8 :     crypto_sha256(buf, s1 * 2 + plen, inner);
      76              : 
      77              :     /* SH(inner, salt2) = PH1 */
      78              :     uint8_t ph1[32];
      79            8 :     memcpy(buf, salt2, s2);
      80            8 :     memcpy(buf + s2, inner, 32);
      81            8 :     memcpy(buf + s2 + 32, salt2, s2);
      82            8 :     crypto_sha256(buf, s2 * 2 + 32, ph1);
      83              : 
      84              :     /* PBKDF2 */
      85              :     uint8_t pb[64];
      86            8 :     crypto_pbkdf2_hmac_sha512(ph1, 32, salt1, s1, 100000, pb, 64);
      87              : 
      88              :     /* SH(pb, salt2) = PH2 */
      89            8 :     memcpy(buf, salt2, s2);
      90            8 :     memcpy(buf + s2, pb, 64);
      91            8 :     memcpy(buf + s2 + 64, salt2, s2);
      92            8 :     crypto_sha256(buf, s2 * 2 + 64, out_x);
      93              : 
      94            8 :     free(buf);
      95            8 : }
      96              : 
      97              : /* S_server = (A * v^u)^b mod p, where v = g^x mod p.
      98              :  * This is the standard SRP-6a server derivation. */
      99            2 : static void server_compute_S(const uint8_t A[256], const uint8_t *b, size_t blen,
     100              :                               const uint8_t *u32, const uint8_t x32[32],
     101              :                               int32_t g_int,
     102              :                               uint8_t S_out[256]) {
     103            2 :     BN_CTX *ctx = BN_CTX_new();
     104            2 :     BIGNUM *p = BN_bin2bn(MTPROTO_P, 256, NULL);
     105            2 :     BIGNUM *g = BN_new();
     106            2 :     BN_set_word(g, (BN_ULONG)g_int);
     107            2 :     BIGNUM *A_bn = BN_bin2bn(A, 256, NULL);
     108            2 :     BIGNUM *x_bn = BN_bin2bn(x32, 32, NULL);
     109            2 :     BIGNUM *u_bn = BN_bin2bn(u32, 32, NULL);
     110            2 :     BIGNUM *b_bn = BN_bin2bn(b, blen, NULL);
     111              : 
     112              :     /* v = g^x mod p */
     113            2 :     BIGNUM *v = BN_new();
     114            2 :     BN_mod_exp(v, g, x_bn, p, ctx);
     115              : 
     116              :     /* tmp = v^u mod p */
     117            2 :     BIGNUM *tmp = BN_new();
     118            2 :     BN_mod_exp(tmp, v, u_bn, p, ctx);
     119              : 
     120              :     /* tmp = (A * tmp) mod p */
     121            2 :     BN_mod_mul(tmp, A_bn, tmp, p, ctx);
     122              : 
     123              :     /* S = tmp^b mod p */
     124            2 :     BIGNUM *S = BN_new();
     125            2 :     BN_mod_exp(S, tmp, b_bn, p, ctx);
     126              : 
     127            2 :     int sz = BN_num_bytes(S);
     128            2 :     memset(S_out, 0, 256);
     129            2 :     BN_bn2bin(S, S_out + (256 - sz));
     130              : 
     131            2 :     BN_free(p); BN_free(g); BN_free(A_bn); BN_free(x_bn);
     132            2 :     BN_free(u_bn); BN_free(b_bn); BN_free(v); BN_free(tmp); BN_free(S);
     133            2 :     BN_CTX_free(ctx);
     134            2 : }
     135              : 
     136              : /* Fill the salt/g/p parts of an Account2faPassword, leave srp_B empty. */
     137            6 : static void init_params_shell(Account2faPassword *p, int32_t g) {
     138            6 :     memset(p, 0, sizeof(*p));
     139            6 :     p->has_password = 1;
     140            6 :     p->srp_id = 0xD00DEDBEEFBADF00LL;
     141            6 :     p->g = g;
     142            6 :     memcpy(p->p, MTPROTO_P, 256);
     143            6 :     p->salt1_len = 16; p->salt2_len = 16;
     144          102 :     for (int i = 0; i < 16; i++) {
     145           96 :         p->salt1[i] = (uint8_t)(0xA0 + i);
     146           96 :         p->salt2[i] = (uint8_t)(0x50 + i);
     147              :     }
     148            6 : }
     149              : 
     150              : /* Compute B = g^b mod p using OpenSSL so the test has a valid server
     151              :  * ephemeral; Telegram clients don't care where B came from as long
     152              :  * as the math works out. The "k" step in SRP-6a is subsumed on the
     153              :  * server side when we recompute S with (A * v^u)^b so we don't need
     154              :  * to emit k*v from the server. */
     155            0 : static void server_B(int32_t g_int, const uint8_t *b, size_t blen,
     156              :                       uint8_t B_out[256]) {
     157            0 :     BN_CTX *ctx = BN_CTX_new();
     158            0 :     BIGNUM *p = BN_bin2bn(MTPROTO_P, 256, NULL);
     159            0 :     BIGNUM *g = BN_new(); BN_set_word(g, (BN_ULONG)g_int);
     160            0 :     BIGNUM *b_bn = BN_bin2bn(b, blen, NULL);
     161            0 :     BIGNUM *B = BN_new();
     162            0 :     BN_mod_exp(B, g, b_bn, p, ctx);
     163            0 :     int sz = BN_num_bytes(B);
     164            0 :     memset(B_out, 0, 256);
     165            0 :     BN_bn2bin(B, B_out + (256 - sz));
     166            0 :     BN_free(p); BN_free(g); BN_free(b_bn); BN_free(B);
     167            0 :     BN_CTX_free(ctx);
     168            0 : }
     169              : 
     170              : /* Telegram's "real" server additionally folds k*v into B before
     171              :  * sending it. Reproduce that here so the test matches production
     172              :  * semantics: B_wire = (k*v + g^b) mod p. */
     173            6 : static void server_B_with_kv(int32_t g_int, const uint8_t *b, size_t blen,
     174              :                               const char *password,
     175              :                               const uint8_t *salt1, size_t s1,
     176              :                               const uint8_t *salt2, size_t s2,
     177              :                               uint8_t B_wire_out[256]) {
     178            6 :     BN_CTX *ctx = BN_CTX_new();
     179            6 :     BIGNUM *p = BN_bin2bn(MTPROTO_P, 256, NULL);
     180            6 :     BIGNUM *g = BN_new(); BN_set_word(g, (BN_ULONG)g_int);
     181            6 :     BIGNUM *b_bn = BN_bin2bn(b, blen, NULL);
     182              : 
     183              :     /* x = PH2, v = g^x mod p */
     184              :     uint8_t x32[32];
     185            6 :     ph2(password, salt1, s1, salt2, s2, x32);
     186            6 :     BIGNUM *x_bn = BN_bin2bn(x32, 32, NULL);
     187            6 :     BIGNUM *v_bn = BN_new();
     188            6 :     BN_mod_exp(v_bn, g, x_bn, p, ctx);
     189              : 
     190              :     /* g_bytes: pad g to 256 bytes (big-endian) */
     191            6 :     uint8_t g_bytes[256]; memset(g_bytes, 0, 256);
     192            6 :     uint32_t gu = (uint32_t)g_int;
     193            6 :     g_bytes[252] = (uint8_t)(gu >> 24);
     194            6 :     g_bytes[253] = (uint8_t)(gu >> 16);
     195            6 :     g_bytes[254] = (uint8_t)(gu >> 8);
     196            6 :     g_bytes[255] = (uint8_t)(gu);
     197              :     /* k = SHA256(p | g_bytes) */
     198            6 :     uint8_t k_buf[512]; memcpy(k_buf, MTPROTO_P, 256); memcpy(k_buf + 256, g_bytes, 256);
     199            6 :     uint8_t k32[32]; crypto_sha256(k_buf, sizeof(k_buf), k32);
     200            6 :     BIGNUM *k_bn = BN_bin2bn(k32, 32, NULL);
     201              : 
     202              :     /* kv = k*v mod p */
     203            6 :     BIGNUM *kv = BN_new();
     204            6 :     BN_mod_mul(kv, k_bn, v_bn, p, ctx);
     205              : 
     206              :     /* gb = g^b mod p */
     207            6 :     BIGNUM *gb = BN_new();
     208            6 :     BN_mod_exp(gb, g, b_bn, p, ctx);
     209              : 
     210              :     /* B_wire = (kv + gb) mod p */
     211            6 :     BIGNUM *B = BN_new();
     212            6 :     BN_mod_add(B, kv, gb, p, ctx);
     213              : 
     214            6 :     int sz = BN_num_bytes(B);
     215            6 :     memset(B_wire_out, 0, 256);
     216            6 :     BN_bn2bin(B, B_wire_out + (256 - sz));
     217              : 
     218            6 :     BN_free(p); BN_free(g); BN_free(b_bn); BN_free(x_bn);
     219            6 :     BN_free(v_bn); BN_free(k_bn); BN_free(kv);
     220            6 :     BN_free(gb); BN_free(B);
     221            6 :     BN_CTX_free(ctx);
     222            6 : }
     223              : 
     224              : /* Full round-trip: client computes (A, M1). Server, holding its own
     225              :  * private b, derives its own S_server using (A * v^u)^b mod p, and
     226              :  * we assert that S_client (derived by the client through the chained
     227              :  * identity base^a * base^(u*x) mod p) also equals that value by
     228              :  * reconstructing K / M1 on the server side. */
     229            2 : static void test_srp_roundtrip_math(void) {
     230            2 :     const char *password = "correct horse battery staple";
     231              : 
     232              :     /* Deterministic client private a and server private b. */
     233          514 :     uint8_t a[256]; for (int i = 0; i < 256; i++) a[i] = (uint8_t)(i ^ 0x11);
     234          514 :     uint8_t b[256]; for (int i = 0; i < 256; i++) b[i] = (uint8_t)(i ^ 0x55);
     235              : 
     236              :     Account2faPassword params;
     237            2 :     init_params_shell(&params, 3);
     238              :     /* Server ephemeral: Telegram convention is B_wire = (k*v + g^b) mod p. */
     239            2 :     server_B_with_kv(3, b, sizeof(b), password,
     240              :                       params.salt1, params.salt1_len,
     241              :                       params.salt2, params.salt2_len,
     242              :                       params.srp_B);
     243            2 :     uint8_t B[256]; memcpy(B, params.srp_B, 256);
     244              : 
     245              :     /* Client side — our project's srp_compute. */
     246              :     uint8_t A[256], M1[32];
     247            2 :     int rc = auth_2fa_srp_compute(&params, password, a, A, M1);
     248            2 :     ASSERT(rc == 0, "client srp_compute ok");
     249              : 
     250              :     /* u = SHA256(A | B) */
     251            2 :     uint8_t ubuf[512]; memcpy(ubuf, A, 256); memcpy(ubuf + 256, B, 256);
     252            2 :     uint8_t u32[32]; crypto_sha256(ubuf, 512, u32);
     253              : 
     254              :     /* x = PH2 */
     255              :     uint8_t x32[32];
     256            2 :     ph2(password, params.salt1, params.salt1_len,
     257              :          params.salt2, params.salt2_len, x32);
     258              : 
     259              :     /* Server S = (A * v^u)^b mod p. */
     260              :     uint8_t S_server[256];
     261            2 :     server_compute_S(A, b, sizeof(b), u32, x32, params.g, S_server);
     262              : 
     263              :     /* Rebuild server-side M1 = H(H(p) XOR H(g) | H(s1) | H(s2) | A | B | H(S)). */
     264            2 :     uint8_t g_bytes[256]; memset(g_bytes, 0, 256); g_bytes[255] = 3;
     265            2 :     uint8_t h_p[32]; crypto_sha256(params.p, 256, h_p);
     266            2 :     uint8_t h_g[32]; crypto_sha256(g_bytes, 256, h_g);
     267           66 :     for (int i = 0; i < 32; i++) h_p[i] ^= h_g[i];
     268            2 :     uint8_t h_s1[32]; crypto_sha256(params.salt1, params.salt1_len, h_s1);
     269            2 :     uint8_t h_s2[32]; crypto_sha256(params.salt2, params.salt2_len, h_s2);
     270            2 :     uint8_t K[32];    crypto_sha256(S_server, 256, K);
     271              : 
     272            2 :     uint8_t m1buf[32 + 32 + 32 + 256 + 256 + 32]; size_t off = 0;
     273            2 :     memcpy(m1buf + off, h_p, 32);  off += 32;
     274            2 :     memcpy(m1buf + off, h_s1, 32); off += 32;
     275            2 :     memcpy(m1buf + off, h_s2, 32); off += 32;
     276            2 :     memcpy(m1buf + off, A, 256);   off += 256;
     277            2 :     memcpy(m1buf + off, B, 256);   off += 256;
     278            2 :     memcpy(m1buf + off, K, 32);    off += 32;
     279            2 :     uint8_t M1_expected[32]; crypto_sha256(m1buf, off, M1_expected);
     280              : 
     281            2 :     ASSERT(memcmp(M1, M1_expected, 32) == 0,
     282              :            "client M1 matches independently derived server M1 — "
     283              :            "SRP math invariant holds");
     284              : }
     285              : 
     286              : /* Changing the password must break the M1 match. */
     287            2 : static void test_srp_wrong_password_breaks_M1(void) {
     288          514 :     uint8_t a[256]; for (int i = 0; i < 256; i++) a[i] = (uint8_t)(i ^ 0x11);
     289          514 :     uint8_t b[256]; for (int i = 0; i < 256; i++) b[i] = (uint8_t)(i ^ 0x55);
     290              : 
     291              :     Account2faPassword params;
     292            2 :     init_params_shell(&params, 3);
     293            2 :     server_B_with_kv(3, b, sizeof(b), "the right one",
     294              :                       params.salt1, params.salt1_len,
     295              :                       params.salt2, params.salt2_len,
     296              :                       params.srp_B);
     297              : 
     298              :     uint8_t A_right[256], M1_right[32];
     299              :     uint8_t A_wrong[256], M1_wrong[32];
     300            2 :     ASSERT(auth_2fa_srp_compute(&params, "the right one", a,
     301              :                                   A_right, M1_right) == 0, "right ok");
     302            2 :     ASSERT(auth_2fa_srp_compute(&params, "the wrong one", a,
     303              :                                   A_wrong, M1_wrong) == 0, "wrong ok");
     304              : 
     305            2 :     ASSERT(memcmp(A_right, A_wrong, 256) == 0,
     306              :            "A depends only on a, not on password");
     307            2 :     ASSERT(memcmp(M1_right, M1_wrong, 32) != 0,
     308              :            "M1 must differ for different passwords");
     309              : }
     310              : 
     311              : /* Deterministic: same inputs → same outputs. */
     312            2 : static void test_srp_deterministic(void) {
     313          514 :     uint8_t a[256]; for (int i = 0; i < 256; i++) a[i] = (uint8_t)(i ^ 0x33);
     314          514 :     uint8_t b[256]; for (int i = 0; i < 256; i++) b[i] = (uint8_t)(i ^ 0x77);
     315              : 
     316              :     Account2faPassword params;
     317            2 :     init_params_shell(&params, 3);
     318            2 :     server_B_with_kv(3, b, sizeof(b), "hunter2",
     319              :                       params.salt1, params.salt1_len,
     320              :                       params.salt2, params.salt2_len, params.srp_B);
     321              : 
     322              :     uint8_t A1[256], M1_1[32], A2[256], M1_2[32];
     323            2 :     ASSERT(auth_2fa_srp_compute(&params, "hunter2", a, A1, M1_1) == 0, "#1");
     324            2 :     ASSERT(auth_2fa_srp_compute(&params, "hunter2", a, A2, M1_2) == 0, "#2");
     325            2 :     ASSERT(memcmp(A1, A2, 256) == 0, "A deterministic");
     326            2 :     ASSERT(memcmp(M1_1, M1_2, 32) == 0, "M1 deterministic");
     327              : }
     328              : 
     329              : /* Bail on missing password flag / NULL arguments. */
     330            2 : static void test_srp_bad_args(void) {
     331              :     uint8_t A[256], M1[32];
     332            2 :     Account2faPassword p = {0};
     333            2 :     ASSERT(auth_2fa_srp_compute(NULL, "x", NULL, A, M1) == -1, "null params");
     334            2 :     ASSERT(auth_2fa_srp_compute(&p, NULL, NULL, A, M1) == -1, "null password");
     335              :     /* has_password == 0 */
     336            2 :     ASSERT(auth_2fa_srp_compute(&p, "x", NULL, A, M1) == -1, "no 2FA set");
     337              : }
     338              : 
     339              : /* Suppress unused-warning for the helper that we keep around for
     340              :  * documentation / future vectors. */
     341            0 : static void suppress_unused(void) { uint8_t b[8]; uint8_t out[256];
     342            0 :     for (int i = 0; i < 8; i++) b[i] = 0;
     343            0 :     server_B(2, b, 1, out); (void)out; }
     344              : 
     345            2 : void run_srp_roundtrip_functional_tests(void) {
     346            2 :     RUN_TEST(test_srp_roundtrip_math);
     347            2 :     RUN_TEST(test_srp_wrong_password_breaks_M1);
     348            2 :     RUN_TEST(test_srp_deterministic);
     349            2 :     RUN_TEST(test_srp_bad_args);
     350              :     (void)suppress_unused;
     351            2 : }
        

Generated by: LCOV version 2.0-1