Coverage Report

Created: 2026-06-09 06:49

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/dovecot/src/lib-auth/auth-scram-client.c
Line
Count
Source
1
/* Copyright (c) 2021-2023 Dovecot authors, see the included COPYING file */
2
3
#include "lib.h"
4
#include "str.h"
5
#include "strnum.h"
6
#include "buffer.h"
7
#include "array.h"
8
#include "base64.h"
9
#include "hmac.h"
10
#include "sha1.h"
11
#include "sha2.h"
12
#include "randgen.h"
13
#include "safe-memset.h"
14
15
#include "auth-gs2.h"
16
#include "auth-scram-client.h"
17
18
/* c-nonce length */
19
#define SCRAM_CLIENT_NONCE_LEN 64
20
/* Max iteration count accepted by the client */
21
#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
22
#  define SCRAM_MAX_ITERATE_COUNT (128 * 4096)
23
#else
24
0
#  define SCRAM_MAX_ITERATE_COUNT (2 * 4096)
25
#endif
26
27
void auth_scram_client_init(struct auth_scram_client *client_r, pool_t pool,
28
          const struct auth_scram_client_settings *set)
29
0
{
30
0
  i_assert(set->hash_method != NULL);
31
32
0
  i_zero(client_r);
33
0
  client_r->pool = pool;
34
0
  client_r->set = *set;
35
0
}
36
37
void auth_scram_client_deinit(struct auth_scram_client *client)
38
0
{
39
0
  if (client->server_signature != NULL) {
40
0
    i_assert(client->set.hash_method != NULL);
41
0
    safe_memset(client->server_signature, 0,
42
0
          client->set.hash_method->digest_size);
43
0
  }
44
0
}
45
46
static void
47
auth_scram_generate_cnonce(struct auth_scram_client *client)
48
0
{
49
0
  unsigned char cnonce[SCRAM_CLIENT_NONCE_LEN+1];
50
0
  size_t i;
51
52
0
  random_fill(cnonce, sizeof(cnonce)-1);
53
54
  /* Make sure cnonce is printable and does not contain ',' */
55
0
  for (i = 0; i < sizeof(cnonce) - 1; i++) {
56
0
    cnonce[i] = (cnonce[i] % ('~' - '!')) + '!';
57
0
    if (cnonce[i] == ',')
58
0
      cnonce[i] = '~';
59
0
  }
60
0
  cnonce[sizeof(cnonce)-1] = '\0';
61
0
  client->nonce = p_strdup(client->pool, (char *)cnonce);
62
0
}
63
64
static string_t *auth_scram_get_client_first(struct auth_scram_client *client)
65
0
{
66
0
  const char *cbind_type = client->set.cbind_type;
67
0
  enum auth_scram_cbind_server_support cbind_support =
68
0
    client->set.cbind_support;
69
0
  struct auth_gs2_header gs2_header;
70
0
  const char *cfm, *cfm_bare;
71
0
  string_t *str;
72
0
  size_t cfm_bare_offset;
73
74
  /* RFC 5802, Section 7:
75
76
     client-first-message = gs2-header client-first-message-bare
77
     gs2-header      = gs2-cbind-flag "," [ authzid ] ","
78
79
     gs2-cbind-flag  = ("p=" cb-name) / "n" / "y"
80
81
     authzid         = "a=" saslname
82
                       ;; Protocol specific.
83
84
     client-first-message-bare = [reserved-mext ","]
85
                       username "," nonce ["," extensions]
86
87
     username        = "n=" saslname
88
89
     nonce           = "r=" c-nonce [s-nonce]
90
91
     extensions      = attr-val *("," attr-val)
92
                       ;; All extensions are optional,
93
                       ;; i.e., unrecognized attributes
94
                       ;; not defined in this document
95
                       ;; MUST be ignored.
96
     attr-val        = ALPHA "=" value
97
   */
98
99
0
  auth_scram_generate_cnonce(client);
100
101
0
  i_zero(&gs2_header);
102
0
  if (cbind_type == NULL) {
103
0
    gs2_header.cbind.status =
104
0
      AUTH_GS2_CBIND_STATUS_NO_CLIENT_SUPPORT;
105
0
  } else if (cbind_support == AUTH_SCRAM_CBIND_SERVER_SUPPORT_NONE) {
106
0
    gs2_header.cbind.status =
107
0
      AUTH_GS2_CBIND_STATUS_NO_SERVER_SUPPORT;
108
0
  } else {
109
0
    gs2_header.cbind.status = AUTH_GS2_CBIND_STATUS_PROVIDED;
110
0
    gs2_header.cbind.name = cbind_type;
111
0
  }
112
0
  gs2_header.authzid = client->set.authzid,
113
114
0
  str = t_str_new(256);
115
0
  auth_gs2_header_encode(&gs2_header, str);
116
117
0
  cfm_bare_offset = str_len(str);
118
0
  str_append(str, "n=");
119
0
  auth_gs2_encode_username(client->set.authid, str);
120
0
  str_append(str, ",r=");
121
0
  str_append(str, client->nonce);
122
123
0
  cfm = str_c(str);
124
0
  cfm_bare = cfm + cfm_bare_offset;
125
126
0
  client->gs2_header = p_strndup(client->pool, cfm, cfm_bare_offset);
127
0
  client->client_first_message_bare = p_strdup(client->pool, cfm_bare);
128
0
  return str;
129
0
}
130
131
static int
132
auth_scram_parse_server_first(struct auth_scram_client *client,
133
            const unsigned char *input, size_t input_len,
134
            const char **error_r)
135
0
{
136
0
  const char **fields;
137
0
  unsigned int field_count, iter;
138
0
  const char *nonce, *salt, *iter_str;
139
0
  size_t salt_len;
140
141
  /* RFC 5802, Section 7:
142
143
     server-first-message =
144
                       [reserved-mext ","] nonce "," salt ","
145
                       iteration-count ["," extensions]
146
   */
147
148
0
  fields = t_strsplit(t_strndup(input, input_len), ",");
149
0
  field_count = str_array_length(fields);
150
0
  if (field_count < 3) {
151
0
    *error_r = "Invalid first server message";
152
0
    return -1;
153
0
  }
154
155
0
  nonce = fields[0];
156
0
  salt = fields[1];
157
0
  iter_str = fields[2];
158
159
  /* reserved-mext   = "m=" 1*(value-char)
160
   */
161
0
  if (nonce[0] == 'm') {
162
0
    *error_r = "Mandatory extension(s) not supported";
163
0
    return -1;
164
0
  }
165
166
  /* nonce           = "r=" c-nonce [s-nonce]
167
                       ;; Second part provided by server.
168
     c-nonce         = printable
169
     s-nonce         = printable
170
   */
171
0
  if (nonce[0] != 'r' || nonce[1] != '=') {
172
0
    *error_r = "Invalid nonce field in first server message";
173
0
    return -1;
174
0
  }
175
0
  if (!str_begins_with(&nonce[2], client->nonce)) {
176
0
    *error_r = "Incorrect nonce in first server message";
177
0
    return -1;
178
0
  }
179
0
  nonce += 2;
180
181
  /* salt            = "s=" base64
182
   */
183
0
  if (salt[0] != 's' || salt[1] != '=') {
184
0
    *error_r = "Invalid salt field in first server message";
185
0
    return -1;
186
0
  }
187
0
  salt_len = strlen(&salt[2]);
188
0
  client->salt = buffer_create_dynamic(
189
0
    client->pool, MAX_BASE64_DECODED_SIZE(salt_len));
190
0
  if (base64_decode(&salt[2], salt_len, client->salt) < 0) {
191
0
    *error_r = "Invalid base64 encoding for salt field in first server message";
192
0
    return -1;
193
0
  }
194
195
  /* iteration-count = "i=" posit-number
196
                       ;; A positive number.
197
   */
198
0
  if (iter_str[0] != 'i' || iter_str[1] != '=' ||
199
0
      str_to_uint(&iter_str[2], &iter) < 0) {
200
0
    *error_r = "Invalid iteration count field in first server message";
201
0
    return -1;
202
0
  }
203
0
  if (iter > SCRAM_MAX_ITERATE_COUNT) {
204
0
    *error_r = "Iteration count out of range in first server message";
205
0
    return -1;
206
0
  }
207
208
0
  client->server_first_message =
209
0
    p_strndup(client->pool, input, input_len);
210
0
  client->nonce = p_strdup(client->pool, nonce);
211
0
  client->iter = iter;
212
0
  return 0;
213
0
}
214
215
static string_t *auth_scram_get_client_final(struct auth_scram_client *client)
216
0
{
217
0
  const struct hash_method *hmethod = client->set.hash_method;
218
219
0
  i_assert(hmethod != NULL);
220
0
  i_assert(client->salt != NULL);
221
222
0
  const buffer_t *cbind_data = client->set.cbind_data;
223
0
  unsigned char salted_password[hmethod->digest_size];
224
0
  unsigned char client_key[hmethod->digest_size];
225
0
  unsigned char stored_key[hmethod->digest_size];
226
0
  unsigned char client_signature[hmethod->digest_size];
227
0
  unsigned char client_proof[hmethod->digest_size];
228
0
  unsigned char server_key[hmethod->digest_size];
229
0
  struct hmac_context ctx;
230
0
  const void *cbind_input;
231
0
  size_t cbind_input_size;
232
0
  string_t *auth_message, *str;
233
0
  unsigned int k;
234
235
  /* RFC 5802, Section 7:
236
237
     client-final-message-without-proof =
238
                       channel-binding "," nonce [","
239
                       extensions]
240
241
     channel-binding = "c=" base64
242
                       ;; base64 encoding of cbind-input.
243
244
     cbind-data      = 1*OCTET
245
     cbind-input     = gs2-header [ cbind-data ]
246
                       ;; cbind-data MUST be present for
247
                       ;; gs2-cbind-flag of "p" and MUST be absent
248
                       ;; for "y" or "n".
249
250
     nonce           = "r=" c-nonce [s-nonce]
251
                       ;; Second part provided by server.
252
     c-nonce         = printable
253
     s-nonce         = printable
254
   */
255
256
0
  if (client->gs2_header[0] != 'p') {
257
0
    i_assert(cbind_data == NULL);
258
0
    cbind_input = client->gs2_header;
259
0
    cbind_input_size = strlen(client->gs2_header);
260
0
  } else {
261
0
    size_t gs2_header_len = strlen(client->gs2_header);
262
0
    buffer_t *cbind_buf;
263
0
    i_assert(cbind_data != NULL);
264
0
    cbind_buf = t_buffer_create(gs2_header_len + cbind_data->used);
265
0
    buffer_append(cbind_buf, client->gs2_header, gs2_header_len);
266
0
    buffer_append_buf(cbind_buf, cbind_data, 0, SIZE_MAX);
267
0
    cbind_input = cbind_buf->data;
268
0
    cbind_input_size = cbind_buf->used;
269
0
  }
270
0
  str = t_str_new(256);
271
0
  str_append(str, "c=");
272
0
  base64_encode(cbind_input, cbind_input_size, str);
273
0
  str_append(str, ",r=");
274
0
  str_append(str, client->nonce);
275
276
  /* SaltedPassword  := Hi(Normalize(password), salt, i)
277
       FIXME: credentials should be SASLprepped UTF8 data here */
278
0
  auth_scram_hi(hmethod,
279
0
          (const unsigned char *)client->set.password,
280
0
          strlen(client->set.password),
281
0
          client->salt->data, client->salt->used,
282
0
          client->iter, salted_password);
283
284
  /* ClientKey       := HMAC(SaltedPassword, "Client Key") */
285
0
  hmac_init(&ctx, salted_password, sizeof(salted_password), hmethod);
286
0
  hmac_update(&ctx, "Client Key", 10);
287
0
  hmac_final(&ctx, client_key);
288
289
  /* StoredKey       := H(ClientKey) */
290
0
  hash_method_get_digest(hmethod, client_key, sizeof(client_key),
291
0
             stored_key);
292
293
  /* AuthMessage     := client-first-message-bare + "," +
294
                        server-first-message + "," +
295
                        client-final-message-without-proof
296
   */
297
0
  auth_message = t_str_new(512);
298
0
  str_append(auth_message, client->client_first_message_bare);
299
0
  str_append_c(auth_message, ',');
300
0
  str_append(auth_message, client->server_first_message);
301
0
  str_append_c(auth_message, ',');
302
0
  str_append_str(auth_message, str);
303
304
  /* ClientSignature := HMAC(StoredKey, AuthMessage) */
305
0
  hmac_init(&ctx, stored_key, sizeof(stored_key), hmethod);
306
0
  hmac_update(&ctx, str_data(auth_message), str_len(auth_message));
307
0
  hmac_final(&ctx, client_signature);
308
309
  /* ClientProof     := ClientKey XOR ClientSignature */
310
0
  for (k = 0; k < hmethod->digest_size; k++)
311
0
    client_proof[k] = client_key[k] ^ client_signature[k];
312
313
0
  safe_memset(client_key, 0, sizeof(client_key));
314
0
  safe_memset(stored_key, 0, sizeof(stored_key));
315
0
  safe_memset(client_signature, 0, sizeof(client_signature));
316
317
  /* ServerKey       := HMAC(SaltedPassword, "Server Key") */
318
0
  hmac_init(&ctx, salted_password, sizeof(salted_password), hmethod);
319
0
  hmac_update(&ctx, "Server Key", 10);
320
0
  hmac_final(&ctx, server_key);
321
322
  /* ServerSignature := HMAC(ServerKey, AuthMessage) */
323
0
  client->server_signature =
324
0
    p_malloc(client->pool, hmethod->digest_size);
325
0
  hmac_init(&ctx, server_key, sizeof(server_key), hmethod);
326
0
  hmac_update(&ctx, str_data(auth_message), str_len(auth_message));
327
0
  hmac_final(&ctx, client->server_signature);
328
329
0
  safe_memset(salted_password, 0, sizeof(salted_password));
330
331
  /* client-final-message =
332
                       client-final-message-without-proof "," proof
333
334
     proof           = "p=" base64
335
   */
336
0
  str_append(str, ",p=");
337
0
  base64_encode(client_proof, sizeof(client_proof), str);
338
339
0
  return str;
340
0
}
341
342
static int
343
auth_scram_parse_server_final(struct auth_scram_client *client,
344
            const unsigned char *input, size_t input_len,
345
            const char **error_r)
346
0
{
347
0
  const struct hash_method *hmethod = client->set.hash_method;
348
349
0
  i_assert(hmethod != NULL);
350
0
  i_assert(client->server_signature != NULL);
351
352
0
  const char **fields;
353
0
  unsigned int field_count;
354
0
  const char *error, *verifier;
355
0
  string_t *str;
356
357
  /* RFC 5802, Section 7:
358
359
     server-final-message = (server-error / verifier)
360
                       ["," extensions]
361
   */
362
363
0
  fields = t_strsplit(t_strndup(input, input_len), ",");
364
0
  field_count = str_array_length(fields);
365
0
  if (field_count < 1) {
366
0
    *error_r = "Invalid final server message";
367
0
    return -1;
368
0
  }
369
370
0
  error = fields[0];
371
0
  verifier = fields[0];
372
373
  /* server-error = "e=" server-error-value
374
   */
375
0
  if (error[0] == 'e' && error[1] == '=') {
376
0
    *error_r = t_strdup_printf("Server returned error value `%s'",
377
0
             &error[2]);
378
0
    return -1;
379
0
  }
380
381
  /* verifier        = "v=" base64
382
                       ;; base-64 encoded ServerSignature.
383
   */
384
0
  if (verifier[0] != 'v' || verifier[1] != '=') {
385
0
    *error_r = "Invalid verifier field in final server message";
386
0
    return -1;
387
0
  }
388
0
  verifier += 2;
389
390
0
  str = t_str_new(MAX_BASE64_ENCODED_SIZE(hmethod->digest_size));
391
0
  base64_encode(client->server_signature, hmethod->digest_size, str);
392
0
  safe_memset(client->server_signature, 0, hmethod->digest_size);
393
394
0
  bool equal = str_equals_timing_almost_safe(verifier, str_c(str));
395
0
  str_clear_safe(str);
396
397
0
  if (!equal) {
398
0
    *error_r = "Incorrect verifier field in final server message";
399
0
    return -1;
400
0
  }
401
0
  return 0;
402
0
}
403
404
int auth_scram_client_input(struct auth_scram_client *client,
405
          const unsigned char *input, size_t input_len,
406
          const char **error_r)
407
0
{
408
0
  int ret = 0;
409
410
0
  switch (client->state) {
411
0
  case AUTH_SCRAM_CLIENT_STATE_INIT:
412
0
    break;
413
0
  case AUTH_SCRAM_CLIENT_STATE_CLIENT_FIRST:
414
0
    i_unreached();
415
0
  case AUTH_SCRAM_CLIENT_STATE_SERVER_FIRST:
416
0
    ret = auth_scram_parse_server_first(client, input, input_len,
417
0
                error_r);
418
0
    break;
419
0
  case AUTH_SCRAM_CLIENT_STATE_CLIENT_FINAL:
420
0
    i_unreached();
421
0
  case AUTH_SCRAM_CLIENT_STATE_SERVER_FINAL:
422
0
    ret = auth_scram_parse_server_final(client, input, input_len,
423
0
                error_r);
424
0
    break;
425
0
  case AUTH_SCRAM_CLIENT_STATE_CLIENT_FINISH:
426
0
    *error_r = "Server didn't finish authentication";
427
0
    ret = -1;
428
0
    break;
429
0
  case AUTH_SCRAM_CLIENT_STATE_END:
430
0
    i_unreached();
431
0
  }
432
0
  client->state++;
433
434
0
  return ret;
435
0
}
436
437
bool auth_scram_client_state_client_first(struct auth_scram_client *client)
438
0
{
439
0
  return (client->state <= AUTH_SCRAM_CLIENT_STATE_CLIENT_FIRST);
440
0
}
441
442
void auth_scram_client_output(struct auth_scram_client *client,
443
            const unsigned char **output_r,
444
            size_t *output_len_r)
445
0
{
446
0
  string_t *output;
447
448
0
  switch (client->state) {
449
0
  case AUTH_SCRAM_CLIENT_STATE_INIT:
450
0
    client->state = AUTH_SCRAM_CLIENT_STATE_CLIENT_FIRST;
451
    /* Fall through */
452
0
  case AUTH_SCRAM_CLIENT_STATE_CLIENT_FIRST:
453
0
    output = auth_scram_get_client_first(client);
454
0
    *output_r = str_data(output);
455
0
    *output_len_r = str_len(output);
456
0
    break;
457
0
  case AUTH_SCRAM_CLIENT_STATE_SERVER_FIRST:
458
0
    i_unreached();
459
0
  case AUTH_SCRAM_CLIENT_STATE_CLIENT_FINAL:
460
0
    output = auth_scram_get_client_final(client);
461
0
    *output_r = str_data(output);
462
0
    *output_len_r = str_len(output);
463
0
    break;
464
0
  case AUTH_SCRAM_CLIENT_STATE_SERVER_FINAL:
465
0
    i_unreached();
466
0
  case AUTH_SCRAM_CLIENT_STATE_CLIENT_FINISH:
467
0
    *output_r = uchar_empty_ptr;
468
0
    *output_len_r = 0;
469
0
    break;
470
0
  case AUTH_SCRAM_CLIENT_STATE_END:
471
0
    i_unreached();
472
0
  }
473
0
  client->state++;
474
0
}