cram.c 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. /* CRAM-MD5 SASL plugin
  2. * Rob Siemborski
  3. * Tim Martin
  4. */
  5. /*
  6. * Copyright (c) 1998-2016 Carnegie Mellon University. All rights reserved.
  7. *
  8. * Redistribution and use in source and binary forms, with or without
  9. * modification, are permitted provided that the following conditions
  10. * are met:
  11. *
  12. * 1. Redistributions of source code must retain the above copyright
  13. * notice, this list of conditions and the following disclaimer.
  14. *
  15. * 2. Redistributions in binary form must reproduce the above copyright
  16. * notice, this list of conditions and the following disclaimer in
  17. * the documentation and/or other materials provided with the
  18. * distribution.
  19. *
  20. * 3. The name "Carnegie Mellon University" must not be used to
  21. * endorse or promote products derived from this software without
  22. * prior written permission. For permission or any other legal
  23. * details, please contact
  24. * Carnegie Mellon University
  25. * Center for Technology Transfer and Enterprise Creation
  26. * 4615 Forbes Avenue
  27. * Suite 302
  28. * Pittsburgh, PA 15213
  29. * (412) 268-7393, fax: (412) 268-7395
  30. * innovation@andrew.cmu.edu
  31. *
  32. * 4. Redistributions of any form whatsoever must retain the following
  33. * acknowledgment:
  34. * "This product includes software developed by Computing Services
  35. * at Carnegie Mellon University (http://www.cmu.edu/computing/)."
  36. *
  37. * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
  38. * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
  39. * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
  40. * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  41. * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
  42. * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
  43. * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  44. */
  45. #include <config.h>
  46. #include <string.h>
  47. #include <stdlib.h>
  48. #include <stdio.h>
  49. #ifndef macintosh
  50. #include <sys/stat.h>
  51. #endif
  52. #include <fcntl.h>
  53. #include <sasl.h>
  54. #include <saslplug.h>
  55. #include <saslutil.h>
  56. #include "plugin_common.h"
  57. #ifdef macintosh
  58. #error #include <sasl_cram_plugin_decl.h>
  59. #endif
  60. /***************************** Common Section *****************************/
  61. /* convert a string of 8bit chars to it's representation in hex
  62. * using lowercase letters
  63. */
  64. static char *convert16(unsigned char *in, int inlen, const sasl_utils_t *utils)
  65. {
  66. static char hex[]="0123456789abcdef";
  67. int lup;
  68. char *out;
  69. out = utils->malloc(inlen*2+1);
  70. if (out == NULL) return NULL;
  71. for (lup=0; lup < inlen; lup++) {
  72. out[lup*2] = hex[in[lup] >> 4];
  73. out[lup*2+1] = hex[in[lup] & 15];
  74. }
  75. out[lup*2] = 0;
  76. return out;
  77. }
  78. /***************************** Server Section *****************************/
  79. typedef struct server_context {
  80. int state;
  81. char *challenge;
  82. } server_context_t;
  83. static int
  84. crammd5_server_mech_new(void *glob_context __attribute__((unused)),
  85. sasl_server_params_t *sparams,
  86. const char *challenge __attribute__((unused)),
  87. unsigned challen __attribute__((unused)),
  88. void **conn_context)
  89. {
  90. server_context_t *text;
  91. /* holds state are in */
  92. text = sparams->utils->malloc(sizeof(server_context_t));
  93. if (text == NULL) {
  94. MEMERROR( sparams->utils );
  95. return SASL_NOMEM;
  96. }
  97. memset(text, 0, sizeof(server_context_t));
  98. text->state = 1;
  99. *conn_context = text;
  100. return SASL_OK;
  101. }
  102. /*
  103. * Returns the current time (or part of it) in string form
  104. * maximum length=15
  105. */
  106. static char *gettime(sasl_server_params_t *sparams)
  107. {
  108. char *ret;
  109. time_t t;
  110. t=time(NULL);
  111. ret= sparams->utils->malloc(15);
  112. if (ret==NULL) return NULL;
  113. /* the bottom bits are really the only random ones so if
  114. we overflow we don't want to loose them */
  115. snprintf(ret,15,"%lu",t%(0xFFFFFF));
  116. return ret;
  117. }
  118. static char *randomdigits(sasl_server_params_t *sparams)
  119. {
  120. unsigned int num;
  121. char *ret;
  122. unsigned char temp[5]; /* random 32-bit number */
  123. sparams->utils->rand(sparams->utils->rpool,(char *) temp,4);
  124. num=(temp[0] * 256 * 256 * 256) +
  125. (temp[1] * 256 * 256) +
  126. (temp[2] * 256) +
  127. (temp[3] );
  128. ret = sparams->utils->malloc(15); /* there's no way an unsigned can be longer than this right? */
  129. if (ret == NULL) return NULL;
  130. sprintf(ret, "%u", num);
  131. return ret;
  132. }
  133. static int
  134. crammd5_server_mech_step1(server_context_t *text,
  135. sasl_server_params_t *sparams,
  136. const char *clientin __attribute__((unused)),
  137. unsigned clientinlen,
  138. const char **serverout,
  139. unsigned *serveroutlen,
  140. sasl_out_params_t *oparams __attribute__((unused)))
  141. {
  142. char *time, *randdigits;
  143. /* we shouldn't have received anything */
  144. if (clientinlen != 0) {
  145. SETERROR(sparams->utils, "CRAM-MD5 does not accept inital data");
  146. return SASL_BADPROT;
  147. }
  148. /* get time and a random number for the nonce */
  149. time = gettime(sparams);
  150. randdigits = randomdigits(sparams);
  151. if ((time == NULL) || (randdigits == NULL)) {
  152. MEMERROR( sparams->utils );
  153. return SASL_NOMEM;
  154. }
  155. /* allocate some space for the challenge */
  156. text->challenge = sparams->utils->malloc(200 + 1);
  157. if (text->challenge == NULL) {
  158. MEMERROR(sparams->utils);
  159. return SASL_NOMEM;
  160. }
  161. /* create the challenge */
  162. snprintf(text->challenge, 200, "<%s.%s@%s>", randdigits, time,
  163. sparams->serverFQDN);
  164. *serverout = text->challenge;
  165. *serveroutlen = (unsigned) strlen(text->challenge);
  166. /* free stuff */
  167. sparams->utils->free(time);
  168. sparams->utils->free(randdigits);
  169. text->state = 2;
  170. return SASL_CONTINUE;
  171. }
  172. static int
  173. crammd5_server_mech_step2(server_context_t *text,
  174. sasl_server_params_t *sparams,
  175. const char *clientin,
  176. unsigned clientinlen,
  177. const char **serverout __attribute__((unused)),
  178. unsigned *serveroutlen __attribute__((unused)),
  179. sasl_out_params_t *oparams)
  180. {
  181. char *userid = NULL;
  182. sasl_secret_t *sec = NULL;
  183. int pos;
  184. size_t len;
  185. int result = SASL_FAIL;
  186. const char *password_request[] = { SASL_AUX_PASSWORD,
  187. #if defined(OBSOLETE_CRAM_ATTR)
  188. "*cmusaslsecretCRAM-MD5",
  189. #endif
  190. NULL };
  191. struct propval auxprop_values[3];
  192. HMAC_MD5_CTX tmphmac;
  193. HMAC_MD5_STATE md5state;
  194. int clear_md5state = 0;
  195. char *digest_str = NULL;
  196. SASL_UINT4 digest[4];
  197. /* extract userid; everything before last space */
  198. pos = clientinlen-1;
  199. while ((pos > 0) && (clientin[pos] != ' ')) pos--;
  200. if (pos <= 0) {
  201. SETERROR( sparams->utils,"need authentication name");
  202. return SASL_BADPROT;
  203. }
  204. userid = (char *) sparams->utils->malloc(pos+1);
  205. if (userid == NULL) {
  206. MEMERROR( sparams->utils);
  207. return SASL_NOMEM;
  208. }
  209. /* copy authstr out */
  210. memcpy(userid, clientin, pos);
  211. userid[pos] = '\0';
  212. result = sparams->utils->prop_request(sparams->propctx, password_request);
  213. if (result != SASL_OK) goto done;
  214. /* this will trigger the getting of the aux properties */
  215. result = sparams->canon_user(sparams->utils->conn,
  216. userid, 0, SASL_CU_AUTHID | SASL_CU_AUTHZID,
  217. oparams);
  218. if (result != SASL_OK) goto done;
  219. result = sparams->utils->prop_getnames(sparams->propctx,
  220. password_request,
  221. auxprop_values);
  222. if (result < 0 ||
  223. ((!auxprop_values[0].name || !auxprop_values[0].values)
  224. #if defined(OBSOLETE_CRAM_ATTR)
  225. && (!auxprop_values[1].name || !auxprop_values[1].values)
  226. #endif
  227. )) {
  228. /* We didn't find this username */
  229. sparams->utils->seterror(sparams->utils->conn,0,
  230. "no secret in database");
  231. result = sparams->transition ? SASL_TRANS : SASL_NOUSER;
  232. goto done;
  233. }
  234. if (auxprop_values[0].name && auxprop_values[0].values) {
  235. len = strlen(auxprop_values[0].values[0]);
  236. if (len == 0) {
  237. sparams->utils->seterror(sparams->utils->conn,0,
  238. "empty secret");
  239. result = SASL_FAIL;
  240. goto done;
  241. }
  242. sec = sparams->utils->malloc(sizeof(sasl_secret_t) + len);
  243. if (!sec) goto done;
  244. sec->len = (unsigned) len;
  245. strncpy((char *)sec->data, auxprop_values[0].values[0], len + 1);
  246. clear_md5state = 1;
  247. /* Do precalculation on plaintext secret */
  248. sparams->utils->hmac_md5_precalc(&md5state, /* OUT */
  249. sec->data,
  250. sec->len);
  251. #if defined(OBSOLETE_CRAM_ATTR)
  252. } else if (auxprop_values[1].name && auxprop_values[1].values) {
  253. /* We have a precomputed secret */
  254. memcpy(&md5state, auxprop_values[1].values[0],
  255. sizeof(HMAC_MD5_STATE));
  256. #endif
  257. } else {
  258. sparams->utils->seterror(sparams->utils->conn, 0,
  259. "Have neither type of secret");
  260. return SASL_FAIL;
  261. }
  262. /* erase the plaintext password */
  263. sparams->utils->prop_erase(sparams->propctx, password_request[0]);
  264. /* ok this is annoying:
  265. so we have this half-way hmac transform instead of the plaintext
  266. that means we half to:
  267. -import it back into a md5 context
  268. -do an md5update with the nonce
  269. -finalize it
  270. */
  271. sparams->utils->hmac_md5_import(&tmphmac, (HMAC_MD5_STATE *) &md5state);
  272. sparams->utils->MD5Update(&(tmphmac.ictx),
  273. (const unsigned char *) text->challenge,
  274. (unsigned) strlen(text->challenge));
  275. sparams->utils->hmac_md5_final((unsigned char *) &digest, &tmphmac);
  276. /* convert to base 16 with lower case letters */
  277. digest_str = convert16((unsigned char *) digest, 16, sparams->utils);
  278. /* if same then verified
  279. * - we know digest_str is null terminated but clientin might not be
  280. * - verify the length of clientin anyway!
  281. */
  282. len = strlen(digest_str);
  283. if (clientinlen-pos-1 < len ||
  284. strncmp(digest_str, clientin+pos+1, len) != 0) {
  285. sparams->utils->seterror(sparams->utils->conn, 0,
  286. "incorrect digest response");
  287. result = SASL_BADAUTH;
  288. goto done;
  289. }
  290. /* set oparams */
  291. oparams->doneflag = 1;
  292. oparams->mech_ssf = 0;
  293. oparams->maxoutbuf = 0;
  294. oparams->encode_context = NULL;
  295. oparams->encode = NULL;
  296. oparams->decode_context = NULL;
  297. oparams->decode = NULL;
  298. oparams->param_version = 0;
  299. result = SASL_OK;
  300. done:
  301. if (userid) sparams->utils->free(userid);
  302. if (sec) _plug_free_secret(sparams->utils, &sec);
  303. if (digest_str) sparams->utils->free(digest_str);
  304. if (clear_md5state) memset(&md5state, 0, sizeof(md5state));
  305. return result;
  306. }
  307. static int crammd5_server_mech_step(void *conn_context,
  308. sasl_server_params_t *sparams,
  309. const char *clientin,
  310. unsigned clientinlen,
  311. const char **serverout,
  312. unsigned *serveroutlen,
  313. sasl_out_params_t *oparams)
  314. {
  315. server_context_t *text = (server_context_t *) conn_context;
  316. *serverout = NULL;
  317. *serveroutlen = 0;
  318. if (text == NULL) {
  319. return SASL_BADPROT;
  320. }
  321. /* this should be well more than is ever needed */
  322. if (clientinlen > 1024) {
  323. SETERROR(sparams->utils, "CRAM-MD5 input longer than 1024 bytes");
  324. return SASL_BADPROT;
  325. }
  326. switch (text->state) {
  327. case 1:
  328. return crammd5_server_mech_step1(text, sparams,
  329. clientin, clientinlen,
  330. serverout, serveroutlen,
  331. oparams);
  332. case 2:
  333. return crammd5_server_mech_step2(text, sparams,
  334. clientin, clientinlen,
  335. serverout, serveroutlen,
  336. oparams);
  337. default: /* should never get here */
  338. sparams->utils->log(NULL, SASL_LOG_ERR,
  339. "Invalid CRAM-MD5 server step %d\n", text->state);
  340. return SASL_FAIL;
  341. }
  342. return SASL_FAIL; /* should never get here */
  343. }
  344. static void crammd5_server_mech_dispose(void *conn_context,
  345. const sasl_utils_t *utils)
  346. {
  347. server_context_t *text = (server_context_t *) conn_context;
  348. if (!text) return;
  349. if (text->challenge) _plug_free_string(utils,&(text->challenge));
  350. utils->free(text);
  351. }
  352. static sasl_server_plug_t crammd5_server_plugins[] =
  353. {
  354. {
  355. "CRAM-MD5", /* mech_name */
  356. 0, /* max_ssf */
  357. SASL_SEC_NOPLAINTEXT
  358. | SASL_SEC_NOANONYMOUS, /* security_flags */
  359. SASL_FEAT_SERVER_FIRST, /* features */
  360. NULL, /* glob_context */
  361. &crammd5_server_mech_new, /* mech_new */
  362. &crammd5_server_mech_step, /* mech_step */
  363. &crammd5_server_mech_dispose, /* mech_dispose */
  364. NULL, /* mech_free */
  365. NULL, /* setpass */
  366. NULL, /* user_query */
  367. NULL, /* idle */
  368. NULL, /* mech avail */
  369. NULL /* spare */
  370. }
  371. };
  372. int crammd5_server_plug_init(const sasl_utils_t *utils,
  373. int maxversion,
  374. int *out_version,
  375. sasl_server_plug_t **pluglist,
  376. int *plugcount)
  377. {
  378. if (maxversion < SASL_SERVER_PLUG_VERSION) {
  379. SETERROR( utils, "CRAM version mismatch");
  380. return SASL_BADVERS;
  381. }
  382. *out_version = SASL_SERVER_PLUG_VERSION;
  383. *pluglist = crammd5_server_plugins;
  384. *plugcount = 1;
  385. return SASL_OK;
  386. }
  387. /***************************** Client Section *****************************/
  388. typedef struct client_context {
  389. char *out_buf;
  390. unsigned out_buf_len;
  391. } client_context_t;
  392. static int crammd5_client_mech_new(void *glob_context __attribute__((unused)),
  393. sasl_client_params_t *params,
  394. void **conn_context)
  395. {
  396. client_context_t *text;
  397. /* holds state are in */
  398. text = params->utils->malloc(sizeof(client_context_t));
  399. if (text == NULL) {
  400. MEMERROR(params->utils);
  401. return SASL_NOMEM;
  402. }
  403. memset(text, 0, sizeof(client_context_t));
  404. *conn_context = text;
  405. return SASL_OK;
  406. }
  407. static char *make_hashed(sasl_secret_t *sec, char *nonce, int noncelen,
  408. const sasl_utils_t *utils)
  409. {
  410. unsigned char digest[24];
  411. char *in16;
  412. if (sec == NULL) return NULL;
  413. /* do the hmac md5 hash output 128 bits */
  414. utils->hmac_md5((unsigned char *) nonce, noncelen,
  415. sec->data, sec->len, digest);
  416. /* convert that to hex form */
  417. in16 = convert16(digest, 16, utils);
  418. if (in16 == NULL) return NULL;
  419. return in16;
  420. }
  421. static int crammd5_client_mech_step(void *conn_context,
  422. sasl_client_params_t *params,
  423. const char *serverin,
  424. unsigned serverinlen,
  425. sasl_interact_t **prompt_need,
  426. const char **clientout,
  427. unsigned *clientoutlen,
  428. sasl_out_params_t *oparams)
  429. {
  430. client_context_t *text = (client_context_t *) conn_context;
  431. const char *authid = NULL;
  432. sasl_secret_t *password = NULL;
  433. unsigned int free_password = 0; /* set if we need to free password */
  434. int auth_result = SASL_OK;
  435. int pass_result = SASL_OK;
  436. int result;
  437. size_t maxsize;
  438. char *in16 = NULL;
  439. *clientout = NULL;
  440. *clientoutlen = 0;
  441. /* First check for absurd lengths */
  442. if (serverinlen > 1024) {
  443. params->utils->seterror(params->utils->conn, 0,
  444. "CRAM-MD5 input longer than 1024 bytes");
  445. return SASL_BADPROT;
  446. }
  447. /* check if sec layer strong enough */
  448. if (params->props.min_ssf > params->external_ssf) {
  449. SETERROR( params->utils, "SSF requested of CRAM-MD5 plugin");
  450. return SASL_TOOWEAK;
  451. }
  452. /* try to get the userid */
  453. if (oparams->authid == NULL) {
  454. auth_result=_plug_get_authid(params->utils, &authid, prompt_need);
  455. if ((auth_result != SASL_OK) && (auth_result != SASL_INTERACT))
  456. return auth_result;
  457. }
  458. /* try to get the password */
  459. if (password == NULL) {
  460. pass_result=_plug_get_password(params->utils, &password,
  461. &free_password, prompt_need);
  462. if ((pass_result != SASL_OK) && (pass_result != SASL_INTERACT))
  463. return pass_result;
  464. }
  465. /* free prompts we got */
  466. if (prompt_need && *prompt_need) {
  467. params->utils->free(*prompt_need);
  468. *prompt_need = NULL;
  469. }
  470. /* if there are prompts not filled in */
  471. if ((auth_result == SASL_INTERACT) || (pass_result == SASL_INTERACT)) {
  472. /* make the prompt list */
  473. result =
  474. _plug_make_prompts(params->utils, prompt_need,
  475. NULL, NULL,
  476. auth_result == SASL_INTERACT ?
  477. "Please enter your authentication name" : NULL,
  478. NULL,
  479. pass_result == SASL_INTERACT ?
  480. "Please enter your password" : NULL, NULL,
  481. NULL, NULL, NULL,
  482. NULL, NULL, NULL);
  483. if (result != SASL_OK) goto cleanup;
  484. return SASL_INTERACT;
  485. }
  486. if (!password) {
  487. PARAMERROR(params->utils);
  488. return SASL_BADPARAM;
  489. }
  490. result = params->canon_user(params->utils->conn, authid, 0,
  491. SASL_CU_AUTHID | SASL_CU_AUTHZID, oparams);
  492. if (result != SASL_OK) goto cleanup;
  493. /*
  494. * username SP digest (keyed md5 where key is passwd)
  495. */
  496. in16 = make_hashed(password, (char *) serverin, serverinlen,
  497. params->utils);
  498. if (in16 == NULL) {
  499. SETERROR(params->utils, "whoops, make_hashed failed us this time");
  500. result = SASL_FAIL;
  501. goto cleanup;
  502. }
  503. maxsize = 32+1+strlen(oparams->authid)+30;
  504. result = _plug_buf_alloc(params->utils, &(text->out_buf),
  505. &(text->out_buf_len), (unsigned) maxsize);
  506. if (result != SASL_OK) goto cleanup;
  507. snprintf(text->out_buf, maxsize, "%s %s", oparams->authid, in16);
  508. *clientout = text->out_buf;
  509. *clientoutlen = (unsigned) strlen(*clientout);
  510. /* set oparams */
  511. oparams->doneflag = 1;
  512. oparams->mech_ssf = 0;
  513. oparams->maxoutbuf = 0;
  514. oparams->encode_context = NULL;
  515. oparams->encode = NULL;
  516. oparams->decode_context = NULL;
  517. oparams->decode = NULL;
  518. oparams->param_version = 0;
  519. result = SASL_OK;
  520. cleanup:
  521. /* get rid of private information */
  522. if (in16) _plug_free_string(params->utils, &in16);
  523. /* get rid of all sensitive info */
  524. if (free_password) _plug_free_secret(params-> utils, &password);
  525. return result;
  526. }
  527. static void crammd5_client_mech_dispose(void *conn_context,
  528. const sasl_utils_t *utils)
  529. {
  530. client_context_t *text = (client_context_t *) conn_context;
  531. if (!text) return;
  532. if (text->out_buf) utils->free(text->out_buf);
  533. utils->free(text);
  534. }
  535. static sasl_client_plug_t crammd5_client_plugins[] =
  536. {
  537. {
  538. "CRAM-MD5", /* mech_name */
  539. 0, /* max_ssf */
  540. SASL_SEC_NOPLAINTEXT
  541. | SASL_SEC_NOANONYMOUS, /* security_flags */
  542. SASL_FEAT_SERVER_FIRST, /* features */
  543. NULL, /* required_prompts */
  544. NULL, /* glob_context */
  545. &crammd5_client_mech_new, /* mech_new */
  546. &crammd5_client_mech_step, /* mech_step */
  547. &crammd5_client_mech_dispose, /* mech_dispose */
  548. NULL, /* mech_free */
  549. NULL, /* idle */
  550. NULL, /* spare */
  551. NULL /* spare */
  552. }
  553. };
  554. int crammd5_client_plug_init(const sasl_utils_t *utils,
  555. int maxversion,
  556. int *out_version,
  557. sasl_client_plug_t **pluglist,
  558. int *plugcount)
  559. {
  560. if (maxversion < SASL_CLIENT_PLUG_VERSION) {
  561. SETERROR( utils, "CRAM version mismatch");
  562. return SASL_BADVERS;
  563. }
  564. *out_version = SASL_CLIENT_PLUG_VERSION;
  565. *pluglist = crammd5_client_plugins;
  566. *plugcount = 1;
  567. return SASL_OK;
  568. }