summaryrefslogtreecommitdiffstats
path: root/src/auths/cram_md5.c
blob: 2c0616ca27c9cd9a9d13bd93dd51d0d3f5e7d542 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
/*************************************************
*     Exim - an Internet mail transport agent    *
*************************************************/

/* Copyright (c) University of Cambridge 1995 - 2018 */
/* Copyright (c) The Exim Maintainers 2020 */
/* See the file NOTICE for conditions of use and distribution. */


/* The stand-alone version just tests the algorithm. We have to drag
in the MD5 computation functions, without their own stand-alone main
program. */

#ifdef STAND_ALONE
#define CRAM_STAND_ALONE
#include "md5.c"


/* This is the normal, non-stand-alone case */

#else
#include "../exim.h"
#include "cram_md5.h"

/* Options specific to the cram_md5 authentication mechanism. */

optionlist auth_cram_md5_options[] = {
  { "client_name",        opt_stringptr,
      OPT_OFF(auth_cram_md5_options_block, client_name) },
  { "client_secret",      opt_stringptr,
      OPT_OFF(auth_cram_md5_options_block, client_secret) },
  { "server_secret",      opt_stringptr,
      OPT_OFF(auth_cram_md5_options_block, server_secret) }
};

/* Size of the options list. An extern variable has to be used so that its
address can appear in the tables drtables.c. */

int auth_cram_md5_options_count =
  sizeof(auth_cram_md5_options)/sizeof(optionlist);

/* Default private options block for the condition authentication method. */

auth_cram_md5_options_block auth_cram_md5_option_defaults = {
  NULL,             /* server_secret */
  NULL,             /* client_secret */
  NULL              /* client_name */
};


#ifdef MACRO_PREDEF

/* Dummy values */
void auth_cram_md5_init(auth_instance *ablock) {}
int auth_cram_md5_server(auth_instance *ablock, uschar *data) {return 0;}
int auth_cram_md5_client(auth_instance *ablock, void *sx, int timeout,
    uschar *buffer, int buffsize) {return 0;}

#else	/*!MACRO_PREDEF*/


/*************************************************
*          Initialization entry point            *
*************************************************/

/* Called for each instance, after its options have been read, to
enable consistency checks to be done, or anything else that needs
to be set up. */

void
auth_cram_md5_init(auth_instance *ablock)
{
auth_cram_md5_options_block *ob =
  (auth_cram_md5_options_block *)(ablock->options_block);
if (ob->server_secret != NULL) ablock->server = TRUE;
if (ob->client_secret != NULL)
  {
  ablock->client = TRUE;
  if (ob->client_name == NULL) ob->client_name = primary_hostname;
  }
}

#endif	/*!MACRO_PREDEF*/
#endif  /* STAND_ALONE */



#ifndef MACRO_PREDEF
/*************************************************
*      Perform the CRAM-MD5 algorithm            *
*************************************************/

/* The CRAM-MD5 algorithm is described in RFC 2195. It computes

  MD5((secret XOR opad), MD5((secret XOR ipad), challenge))

where secret is padded out to 64 characters (after being reduced to an MD5
digest if longer than 64) and ipad and opad are 64-byte strings of 0x36 and
0x5c respectively, and comma means concatenation.

Arguments:
  secret         the shared secret
  challenge      the challenge text
  digest         16-byte slot to put the answer in

Returns:         nothing
*/

static void
compute_cram_md5(uschar *secret, uschar *challenge, uschar *digestptr)
{
md5 base;
int len = Ustrlen(secret);
uschar isecret[64];
uschar osecret[64];
uschar md5secret[16];

/* If the secret is longer than 64 characters, we compute its MD5 digest
and use that. */

if (len > 64)
  {
  md5_start(&base);
  md5_end(&base, US secret, len, md5secret);
  secret = US md5secret;
  len = 16;
  }

/* The key length is now known to be <= 64. Set up the padded and xor'ed
versions. */

memcpy(isecret, secret, len);
memset(isecret+len, 0, 64-len);
memcpy(osecret, isecret, 64);

for (int i = 0; i < 64; i++)
  {
  isecret[i] ^= 0x36;
  osecret[i] ^= 0x5c;
  }

/* Compute the inner MD5 digest */

md5_start(&base);
md5_mid(&base, isecret);
md5_end(&base, US challenge, Ustrlen(challenge), md5secret);

/* Compute the outer MD5 digest */

md5_start(&base);
md5_mid(&base, osecret);
md5_end(&base, md5secret, 16, digestptr);
}


#ifndef STAND_ALONE

/*************************************************
*             Server entry point                 *
*************************************************/

/* For interface, see auths/README */

int
auth_cram_md5_server(auth_instance *ablock, uschar *data)
{
auth_cram_md5_options_block *ob =
  (auth_cram_md5_options_block *)(ablock->options_block);
uschar *challenge = string_sprintf("<%d.%ld@%s>", getpid(),
    (long int) time(NULL), primary_hostname);
uschar *clear, *secret;
uschar digest[16];
int i, rc, len;

/* If we are running in the test harness, always send the same challenge,
an example string taken from the RFC. */

if (f.running_in_test_harness)
  challenge = US"<1896.697170952@postoffice.reston.mci.net>";

/* No data should have been sent with the AUTH command */

if (*data) return UNEXPECTED;

/* Send the challenge, read the return */

if ((rc = auth_get_data(&data, challenge, Ustrlen(challenge))) != OK) return rc;
if ((len = b64decode(data, &clear)) < 0) return BAD64;

/* The return consists of a user name, space-separated from the CRAM-MD5
digest, expressed in hex. Extract the user name and put it in $auth1 and $1.
The former is now the preferred variable; the latter is the original one. Then
check that the remaining length is 32. */

auth_vars[0] = expand_nstring[1] = clear;
while (*clear && !isspace(*clear)) clear++;
if (!isspace(*clear)) return FAIL;
*clear++ = 0;

expand_nlength[1] = clear - expand_nstring[1] - 1;
if (len - expand_nlength[1] - 1 != 32) return FAIL;
expand_nmax = 1;

/* Expand the server_secret string so that it can compute a value dependent on
the user name if necessary. */

debug_print_string(ablock->server_debug_string);    /* customized debugging */
secret = expand_string(ob->server_secret);

/* A forced fail implies failure of authentication - i.e. we have no secret for
the given name. */

if (secret == NULL)
  {
  if (f.expand_string_forcedfail) return FAIL;
  auth_defer_msg = expand_string_message;
  return DEFER;
  }

/* Compute the CRAM-MD5 digest that we should have received from the client. */

compute_cram_md5(secret, challenge, digest);

HDEBUG(D_auth)
  {
  uschar buff[64];
  debug_printf("CRAM-MD5: user name = %s\n", auth_vars[0]);
  debug_printf("          challenge = %s\n", challenge);
  debug_printf("          received  = %s\n", clear);
  Ustrcpy(buff, US"          digest    = ");
  for (i = 0; i < 16; i++) sprintf(CS buff+22+2*i, "%02x", digest[i]);
  debug_printf("%.54s\n", buff);
  }

/* We now have to compare the digest, which is 16 bytes in binary, with the
data received, which is expressed in lower case hex. We checked above that
there were 32 characters of data left. */

for (i = 0; i < 16; i++)
  {
  int a = *clear++;
  int b = *clear++;
  if (((((a >= 'a')? a - 'a' + 10 : a - '0') << 4) +
        ((b >= 'a')? b - 'a' + 10 : b - '0')) != digest[i]) return FAIL;
  }

/* Expand server_condition as an authorization check */
return auth_check_serv_cond(ablock);
}



/*************************************************
*              Client entry point                *
*************************************************/

/* For interface, see auths/README */

int
auth_cram_md5_client(
  auth_instance *ablock,                 /* authenticator block */
  void * sx,				 /* smtp connextion */
  int timeout,                           /* command timeout */
  uschar *buffer,                        /* for reading response */
  int buffsize)                          /* size of buffer */
{
auth_cram_md5_options_block *ob =
  (auth_cram_md5_options_block *)(ablock->options_block);
uschar *secret = expand_string(ob->client_secret);
uschar *name = expand_string(ob->client_name);
uschar *challenge, *p;
int i;
uschar digest[16];

/* If expansion of either the secret or the user name failed, return CANCELLED
or ERROR, as appropriate. */

if (!secret || !name)
  {
  if (f.expand_string_forcedfail)
    {
    *buffer = 0;           /* No message */
    return CANCELLED;
    }
  string_format(buffer, buffsize, "expansion of \"%s\" failed in "
    "%s authenticator: %s",
    !secret ? ob->client_secret : ob->client_name,
    ablock->name, expand_string_message);
  return ERROR;
  }

/* Initiate the authentication exchange and read the challenge, which arrives
in base 64. */

if (smtp_write_command(sx, SCMD_FLUSH, "AUTH %s\r\n", ablock->public_name) < 0)
  return FAIL_SEND;
if (!smtp_read_response(sx, buffer, buffsize, '3', timeout))
  return FAIL;

if (b64decode(buffer + 4, &challenge) < 0)
  {
  string_format(buffer, buffsize, "bad base 64 string in challenge: %s",
    big_buffer + 4);
  return ERROR;
  }

/* Run the CRAM-MD5 algorithm on the secret and the challenge */

compute_cram_md5(secret, challenge, digest);

/* Create the response from the user name plus the CRAM-MD5 digest */

string_format(big_buffer, big_buffer_size - 36, "%s", name);
for (p = big_buffer; *p; ) p++;
*p++ = ' ';

for (i = 0; i < 16; i++)
  p += sprintf(CS p, "%02x", digest[i]);

/* Send the response, in base 64, and check the result. The response is
in big_buffer, but b64encode() returns its result in working store,
so calling smtp_write_command(), which uses big_buffer, is OK. */

buffer[0] = 0;
if (smtp_write_command(sx, SCMD_FLUSH, "%s\r\n", b64encode(CUS big_buffer,
  p - big_buffer)) < 0) return FAIL_SEND;

return smtp_read_response(sx, US buffer, buffsize, '2', timeout)
  ? OK : FAIL;
}
#endif  /* STAND_ALONE */


/*************************************************
**************************************************
*             Stand-alone test program           *
**************************************************
*************************************************/

#ifdef STAND_ALONE

int main(int argc, char **argv)
{
int i;
uschar *secret = US argv[1];
uschar *challenge = US argv[2];
uschar digest[16];

compute_cram_md5(secret, challenge, digest);

for (i = 0; i < 16; i++) printf("%02x", digest[i]);
printf("\n");

return 0;
}

#endif

#endif	/*!MACRO_PREDEF*/
/* End of cram_md5.c */