1 /*************************************************
2 * Exim - an Internet mail transport agent *
3 *************************************************/
5 /* Copyright (c) The Exim Maintainers 2020 - 2023 */
6 /* Copyright (c) University of Cambridge 1995 - 2018 */
7 /* See the file NOTICE for conditions of use and distribution. */
8 /* SPDX-License-Identifier: GPL-2.0-or-later */
11 /* The stand-alone version just tests the algorithm. We have to drag
12 in the MD5 computation functions, without their own stand-alone main
16 # define CRAM_STAND_ALONE
20 /* This is the normal, non-stand-alone case */
26 # include "cram_md5.h"
28 /* Options specific to the cram_md5 authentication mechanism. */
30 optionlist auth_cram_md5_options[] = {
31 { "client_name", opt_stringptr,
32 OPT_OFF(auth_cram_md5_options_block, client_name) },
33 { "client_secret", opt_stringptr,
34 OPT_OFF(auth_cram_md5_options_block, client_secret) },
35 { "server_secret", opt_stringptr,
36 OPT_OFF(auth_cram_md5_options_block, server_secret) }
39 /* Size of the options list. An extern variable has to be used so that its
40 address can appear in the tables drtables.c. */
42 int auth_cram_md5_options_count =
43 sizeof(auth_cram_md5_options)/sizeof(optionlist);
45 /* Default private options block for the condition authentication method. */
47 auth_cram_md5_options_block auth_cram_md5_option_defaults = {
48 NULL, /* server_secret */
49 NULL, /* client_secret */
50 NULL /* client_name */
57 void auth_cram_md5_init(auth_instance *ablock) {}
58 int auth_cram_md5_server(auth_instance *ablock, uschar *data) {return 0;}
59 int auth_cram_md5_client(auth_instance *ablock, void *sx, int timeout,
60 uschar *buffer, int buffsize) {return 0;}
62 # else /*!MACRO_PREDEF*/
65 /*************************************************
66 * Initialization entry point *
67 *************************************************/
69 /* Called for each instance, after its options have been read, to
70 enable consistency checks to be done, or anything else that needs
74 auth_cram_md5_init(auth_instance *ablock)
76 auth_cram_md5_options_block *ob =
77 (auth_cram_md5_options_block *)(ablock->options_block);
78 if (ob->server_secret != NULL) ablock->server = TRUE;
79 if (ob->client_secret != NULL)
81 ablock->client = TRUE;
82 if (ob->client_name == NULL) ob->client_name = primary_hostname;
86 # endif /*!MACRO_PREDEF*/
87 # endif /*AUTH_CRAM_MD5*/
88 #endif /*!STAND_ALONE*/
93 /*************************************************
94 * Perform the CRAM-MD5 algorithm *
95 *************************************************/
97 /* The CRAM-MD5 algorithm is described in RFC 2195. It computes
99 MD5((secret XOR opad), MD5((secret XOR ipad), challenge))
101 where secret is padded out to 64 characters (after being reduced to an MD5
102 digest if longer than 64) and ipad and opad are 64-byte strings of 0x36 and
103 0x5c respectively, and comma means concatenation.
106 secret the shared secret
107 challenge the challenge text
108 digest 16-byte slot to put the answer in
114 compute_cram_md5(uschar *secret, uschar *challenge, uschar *digestptr)
117 int len = Ustrlen(secret);
120 uschar md5secret[16];
122 /* If the secret is longer than 64 characters, we compute its MD5 digest
128 md5_end(&base, US secret, len, md5secret);
129 secret = US md5secret;
133 /* The key length is now known to be <= 64. Set up the padded and xor'ed
136 memcpy(isecret, secret, len);
137 memset(isecret+len, 0, 64-len);
138 memcpy(osecret, isecret, 64);
140 for (int i = 0; i < 64; i++)
146 /* Compute the inner MD5 digest */
149 md5_mid(&base, isecret);
150 md5_end(&base, US challenge, Ustrlen(challenge), md5secret);
152 /* Compute the outer MD5 digest */
155 md5_mid(&base, osecret);
156 md5_end(&base, md5secret, 16, digestptr);
161 # ifdef AUTH_CRAM_MD5
163 /*************************************************
164 * Server entry point *
165 *************************************************/
167 /* For interface, see auths/README */
170 auth_cram_md5_server(auth_instance * ablock, uschar * data)
172 auth_cram_md5_options_block * ob =
173 (auth_cram_md5_options_block *)(ablock->options_block);
174 uschar * challenge = string_sprintf("<%d.%ld@%s>", getpid(),
175 (long int) time(NULL), primary_hostname);
176 uschar * clear, * secret;
180 /* If we are running in the test harness, always send the same challenge,
181 an example string taken from the RFC. */
183 if (f.running_in_test_harness)
184 challenge = US"<1896.697170952@postoffice.reston.mci.net>";
186 /* No data should have been sent with the AUTH command */
188 if (*data) return UNEXPECTED;
190 /* Send the challenge, read the return */
192 if ((rc = auth_get_data(&data, challenge, Ustrlen(challenge))) != OK) return rc;
193 if ((len = b64decode(data, &clear, GET_TAINTED)) < 0) return BAD64;
195 /* The return consists of a user name, space-separated from the CRAM-MD5
196 digest, expressed in hex. Extract the user name and put it in $auth1 and $1.
197 The former is now the preferred variable; the latter is the original one. Then
198 check that the remaining length is 32. */
200 auth_vars[0] = expand_nstring[1] = clear;
201 Uskip_nonwhite(&clear);
202 if (!isspace(*clear)) return FAIL;
205 expand_nlength[1] = clear - expand_nstring[1] - 1;
206 if (len - expand_nlength[1] - 1 != 32) return FAIL;
209 /* Expand the server_secret string so that it can compute a value dependent on
210 the user name if necessary. */
212 debug_print_string(ablock->server_debug_string); /* customized debugging */
213 secret = expand_string(ob->server_secret);
215 /* A forced fail implies failure of authentication - i.e. we have no secret for
220 if (f.expand_string_forcedfail) return FAIL;
221 auth_defer_msg = expand_string_message;
225 /* Compute the CRAM-MD5 digest that we should have received from the client. */
227 compute_cram_md5(secret, challenge, digest);
232 debug_printf("CRAM-MD5: user name = %s\n", auth_vars[0]);
233 debug_printf(" challenge = %s\n", challenge);
234 debug_printf(" received = %s\n", clear);
235 Ustrcpy(buff, US" digest = ");
236 for (i = 0; i < 16; i++) sprintf(CS buff+22+2*i, "%02x", digest[i]);
237 debug_printf("%.54s\n", buff);
240 /* We now have to compare the digest, which is 16 bytes in binary, with the
241 data received, which is expressed in lower case hex. We checked above that
242 there were 32 characters of data left. */
244 for (i = 0; i < 16; i++)
248 if (((((a >= 'a')? a - 'a' + 10 : a - '0') << 4) +
249 ((b >= 'a')? b - 'a' + 10 : b - '0')) != digest[i]) return FAIL;
252 /* Expand server_condition as an authorization check */
253 return auth_check_serv_cond(ablock);
258 /*************************************************
259 * Client entry point *
260 *************************************************/
262 /* For interface, see auths/README */
265 auth_cram_md5_client(
266 auth_instance *ablock, /* authenticator block */
267 void * sx, /* smtp connextion */
268 int timeout, /* command timeout */
269 uschar *buffer, /* for reading response */
270 int buffsize) /* size of buffer */
272 auth_cram_md5_options_block *ob =
273 (auth_cram_md5_options_block *)(ablock->options_block);
274 uschar *secret = expand_string(ob->client_secret);
275 uschar *name = expand_string(ob->client_name);
276 uschar *challenge, *p;
280 /* If expansion of either the secret or the user name failed, return CANCELLED
281 or ERROR, as appropriate. */
283 if (!secret || !name)
285 if (f.expand_string_forcedfail)
287 *buffer = 0; /* No message */
290 string_format(buffer, buffsize, "expansion of \"%s\" failed in "
291 "%s authenticator: %s",
292 !secret ? ob->client_secret : ob->client_name,
293 ablock->name, expand_string_message);
297 /* Initiate the authentication exchange and read the challenge, which arrives
300 if (smtp_write_command(sx, SCMD_FLUSH, "AUTH %s\r\n", ablock->public_name) < 0)
302 if (!smtp_read_response(sx, buffer, buffsize, '3', timeout))
305 if (b64decode(buffer + 4, &challenge, buffer + 4) < 0)
307 string_format(buffer, buffsize, "bad base 64 string in challenge: %s",
312 /* Run the CRAM-MD5 algorithm on the secret and the challenge */
314 compute_cram_md5(secret, challenge, digest);
316 /* Create the response from the user name plus the CRAM-MD5 digest */
318 string_format(big_buffer, big_buffer_size - 36, "%s", name);
319 for (p = big_buffer; *p; ) p++;
322 for (i = 0; i < 16; i++)
323 p += sprintf(CS p, "%02x", digest[i]);
325 /* Send the response, in base 64, and check the result. The response is
326 in big_buffer, but b64encode() returns its result in working store,
327 so calling smtp_write_command(), which uses big_buffer, is OK. */
330 if (smtp_write_command(sx, SCMD_FLUSH, "%s\r\n", b64encode(CUS big_buffer,
331 p - big_buffer)) < 0) return FAIL_SEND;
333 return smtp_read_response(sx, US buffer, buffsize, '2', timeout)
336 # endif /*AUTH_CRAM_MD5*/
337 # endif /*!STAND_ALONE*/
340 /*************************************************
341 **************************************************
342 * Stand-alone test program *
343 **************************************************
344 *************************************************/
348 int main(int argc, char **argv)
351 uschar *secret = US argv[1];
352 uschar *challenge = US argv[2];
355 compute_cram_md5(secret, challenge, digest);
357 for (i = 0; i < 16; i++) printf("%02x", digest[i]);
363 # endif /*STAND_ALONE*/
365 #endif /*!MACRO_PREDEF*/
366 /* End of cram_md5.c */