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(¶ms, 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(¶ms, 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(¶ms, 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(¶ms, "the right one", a,
301 : A_right, M1_right) == 0, "right ok");
302 2 : ASSERT(auth_2fa_srp_compute(¶ms, "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(¶ms, 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(¶ms, "hunter2", a, A1, M1_1) == 0, "#1");
324 2 : ASSERT(auth_2fa_srp_compute(¶ms, "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 : }
|