1 /*************************************************
2 * Exim - an Internet mail transport agent *
3 *************************************************/
5 /* Copyright (c) University of Cambridge 1995 - 2017 */
6 /* See the file NOTICE for conditions of use and distribution. */
9 /* The stand-alone version just tests the algorithm. We have to drag
10 in the MD5 computation functions, without their own stand-alone main
14 #define CRAM_STAND_ALONE
18 /* This is the normal, non-stand-alone case */
24 /* Options specific to the cram_md5 authentication mechanism. */
26 optionlist auth_cram_md5_options[] = {
27 { "client_name", opt_stringptr,
28 (void *)(offsetof(auth_cram_md5_options_block, client_name)) },
29 { "client_secret", opt_stringptr,
30 (void *)(offsetof(auth_cram_md5_options_block, client_secret)) },
31 { "server_secret", opt_stringptr,
32 (void *)(offsetof(auth_cram_md5_options_block, server_secret)) }
35 /* Size of the options list. An extern variable has to be used so that its
36 address can appear in the tables drtables.c. */
38 int auth_cram_md5_options_count =
39 sizeof(auth_cram_md5_options)/sizeof(optionlist);
41 /* Default private options block for the condition authentication method. */
43 auth_cram_md5_options_block auth_cram_md5_option_defaults = {
44 NULL, /* server_secret */
45 NULL, /* client_secret */
46 NULL /* client_name */
53 void auth_cram_md5_init(auth_instance *ablock) {}
54 int auth_cram_md5_server(auth_instance *ablock, uschar *data) {return 0;}
55 int auth_cram_md5_client(auth_instance *ablock, smtp_inblock *inblock,
56 smtp_outblock *outblock, int timeout, uschar *buffer, int buffsize) {return 0;}
58 #else /*!MACRO_PREDEF*/
61 /*************************************************
62 * Initialization entry point *
63 *************************************************/
65 /* Called for each instance, after its options have been read, to
66 enable consistency checks to be done, or anything else that needs
70 auth_cram_md5_init(auth_instance *ablock)
72 auth_cram_md5_options_block *ob =
73 (auth_cram_md5_options_block *)(ablock->options_block);
74 if (ob->server_secret != NULL) ablock->server = TRUE;
75 if (ob->client_secret != NULL)
77 ablock->client = TRUE;
78 if (ob->client_name == NULL) ob->client_name = primary_hostname;
82 #endif /*!MACRO_PREDEF*/
83 #endif /* STAND_ALONE */
88 /*************************************************
89 * Perform the CRAM-MD5 algorithm *
90 *************************************************/
92 /* The CRAM-MD5 algorithm is described in RFC 2195. It computes
94 MD5((secret XOR opad), MD5((secret XOR ipad), challenge))
96 where secret is padded out to 64 characters (after being reduced to an MD5
97 digest if longer than 64) and ipad and opad are 64-byte strings of 0x36 and
98 0x5c respectively, and comma means concatenation.
101 secret the shared secret
102 challenge the challenge text
103 digest 16-byte slot to put the answer in
109 compute_cram_md5(uschar *secret, uschar *challenge, uschar *digestptr)
113 int len = Ustrlen(secret);
116 uschar md5secret[16];
118 /* If the secret is longer than 64 characters, we compute its MD5 digest
124 md5_end(&base, US secret, len, md5secret);
125 secret = US md5secret;
129 /* The key length is now known to be <= 64. Set up the padded and xor'ed
132 memcpy(isecret, secret, len);
133 memset(isecret+len, 0, 64-len);
134 memcpy(osecret, isecret, 64);
136 for (i = 0; i < 64; i++)
142 /* Compute the inner MD5 digest */
145 md5_mid(&base, isecret);
146 md5_end(&base, US challenge, Ustrlen(challenge), md5secret);
148 /* Compute the outer MD5 digest */
151 md5_mid(&base, osecret);
152 md5_end(&base, md5secret, 16, digestptr);
158 /*************************************************
159 * Server entry point *
160 *************************************************/
162 /* For interface, see auths/README */
165 auth_cram_md5_server(auth_instance *ablock, uschar *data)
167 auth_cram_md5_options_block *ob =
168 (auth_cram_md5_options_block *)(ablock->options_block);
169 uschar *challenge = string_sprintf("<%d.%ld@%s>", getpid(),
170 (long int) time(NULL), primary_hostname);
171 uschar *clear, *secret;
175 /* If we are running in the test harness, always send the same challenge,
176 an example string taken from the RFC. */
178 if (running_in_test_harness)
179 challenge = US"<1896.697170952@postoffice.reston.mci.net>";
181 /* No data should have been sent with the AUTH command */
183 if (*data != 0) return UNEXPECTED;
185 /* Send the challenge, read the return */
187 if ((rc = auth_get_data(&data, challenge, Ustrlen(challenge))) != OK) return rc;
188 if ((len = b64decode(data, &clear)) < 0) return BAD64;
190 /* The return consists of a user name, space-separated from the CRAM-MD5
191 digest, expressed in hex. Extract the user name and put it in $auth1 and $1.
192 The former is now the preferred variable; the latter is the original one. Then
193 check that the remaining length is 32. */
195 auth_vars[0] = expand_nstring[1] = clear;
196 while (*clear != 0 && !isspace(*clear)) clear++;
197 if (!isspace(*clear)) return FAIL;
200 expand_nlength[1] = clear - expand_nstring[1] - 1;
201 if (len - expand_nlength[1] - 1 != 32) return FAIL;
204 /* Expand the server_secret string so that it can compute a value dependent on
205 the user name if necessary. */
207 debug_print_string(ablock->server_debug_string); /* customized debugging */
208 secret = expand_string(ob->server_secret);
210 /* A forced fail implies failure of authentication - i.e. we have no secret for
215 if (expand_string_forcedfail) return FAIL;
216 auth_defer_msg = expand_string_message;
220 /* Compute the CRAM-MD5 digest that we should have received from the client. */
222 compute_cram_md5(secret, challenge, digest);
227 debug_printf("CRAM-MD5: user name = %s\n", auth_vars[0]);
228 debug_printf(" challenge = %s\n", challenge);
229 debug_printf(" received = %s\n", clear);
230 Ustrcpy(buff," digest = ");
231 for (i = 0; i < 16; i++) sprintf(CS buff+22+2*i, "%02x", digest[i]);
232 debug_printf("%.54s\n", buff);
235 /* We now have to compare the digest, which is 16 bytes in binary, with the
236 data received, which is expressed in lower case hex. We checked above that
237 there were 32 characters of data left. */
239 for (i = 0; i < 16; i++)
243 if (((((a >= 'a')? a - 'a' + 10 : a - '0') << 4) +
244 ((b >= 'a')? b - 'a' + 10 : b - '0')) != digest[i]) return FAIL;
247 /* Expand server_condition as an authorization check */
248 return auth_check_serv_cond(ablock);
253 /*************************************************
254 * Client entry point *
255 *************************************************/
257 /* For interface, see auths/README */
260 auth_cram_md5_client(
261 auth_instance *ablock, /* authenticator block */
262 smtp_inblock *inblock, /* input connection */
263 smtp_outblock *outblock, /* output connection */
264 int timeout, /* command timeout */
265 uschar *buffer, /* for reading response */
266 int buffsize) /* size of buffer */
268 auth_cram_md5_options_block *ob =
269 (auth_cram_md5_options_block *)(ablock->options_block);
270 uschar *secret = expand_string(ob->client_secret);
271 uschar *name = expand_string(ob->client_name);
272 uschar *challenge, *p;
276 /* If expansion of either the secret or the user name failed, return CANCELLED
277 or ERROR, as appropriate. */
279 if (!secret || !name)
281 if (expand_string_forcedfail)
283 *buffer = 0; /* No message */
286 string_format(buffer, buffsize, "expansion of \"%s\" failed in "
287 "%s authenticator: %s",
288 !secret ? ob->client_secret : ob->client_name,
289 ablock->name, expand_string_message);
293 /* Initiate the authentication exchange and read the challenge, which arrives
296 if (smtp_write_command(outblock, SCMD_FLUSH, "AUTH %s\r\n",
297 ablock->public_name) < 0)
299 if (!smtp_read_response(inblock, buffer, buffsize, '3', timeout))
302 if (b64decode(buffer + 4, &challenge) < 0)
304 string_format(buffer, buffsize, "bad base 64 string in challenge: %s",
309 /* Run the CRAM-MD5 algorithm on the secret and the challenge */
311 compute_cram_md5(secret, challenge, digest);
313 /* Create the response from the user name plus the CRAM-MD5 digest */
315 string_format(big_buffer, big_buffer_size - 36, "%s", name);
316 for (p = big_buffer; *p; ) p++;
319 for (i = 0; i < 16; i++)
320 p += sprintf(CS p, "%02x", digest[i]);
322 /* Send the response, in base 64, and check the result. The response is
323 in big_buffer, but b64encode() returns its result in working store,
324 so calling smtp_write_command(), which uses big_buffer, is OK. */
327 if (smtp_write_command(outblock, SCMD_FLUSH, "%s\r\n", b64encode(big_buffer,
328 p - big_buffer)) < 0) return FAIL_SEND;
330 return smtp_read_response(inblock, US buffer, buffsize, '2', timeout)
333 #endif /* STAND_ALONE */
336 /*************************************************
337 **************************************************
338 * Stand-alone test program *
339 **************************************************
340 *************************************************/
344 int main(int argc, char **argv)
347 uschar *secret = US argv[1];
348 uschar *challenge = US argv[2];
351 compute_cram_md5(secret, challenge, digest);
353 for (i = 0; i < 16; i++) printf("%02x", digest[i]);
361 #endif /*!MACRO_PREDEF*/
362 /* End of cram_md5.c */