1/*++
2/* NAME
3/*	xsasl_cyrus_client 3
4/* SUMMARY
5/*	Cyrus SASL client-side plug-in
6/* SYNOPSIS
7/*	#include <xsasl_cyrus_client.h>
8/*
9/*	XSASL_CLIENT_IMPL *xsasl_cyrus_client_init(client_type, path_info)
10/*	const char *client_type;
11/* DESCRIPTION
12/*	This module implements the Cyrus SASL client-side authentication
13/*	plug-in.
14/*
15/*	xsasl_cyrus_client_init() initializes the Cyrus SASL library and
16/*	returns an implementation handle that can be used to generate
17/*	SASL client instances.
18/*
19/*	Arguments:
20/* .IP client_type
21/*	The plug-in SASL client type (cyrus). This argument is
22/*	ignored, but it could be used when one implementation
23/*	provides multiple variants.
24/* .IP path_info
25/*	Implementation-specific information to specify the location
26/*	of a configuration file, rendez-vous point, etc. This
27/*	information is ignored by the Cyrus SASL client plug-in.
28/* DIAGNOSTICS
29/*	Fatal: out of memory.
30/*
31/*	Panic: interface violation.
32/*
33/*	Other: the routines log a warning and return an error result
34/*	as specified in xsasl_client(3).
35/* SEE ALSO
36/*	xsasl_client(3) Client API
37/* LICENSE
38/* .ad
39/* .fi
40/*	The Secure Mailer license must be distributed with this software.
41/* AUTHOR(S)
42/*	Original author:
43/*	Till Franke
44/*	SuSE Rhein/Main AG
45/*	65760 Eschborn, Germany
46/*
47/*	Adopted by:
48/*	Wietse Venema
49/*	IBM T.J. Watson Research
50/*	P.O. Box 704
51/*	Yorktown Heights, NY 10598, USA
52/*--*/
53
54 /*
55  * System library.
56  */
57#include <sys_defs.h>
58#include <stdlib.h>
59#include <string.h>
60
61 /*
62  * Utility library
63  */
64#include <msg.h>
65#include <mymalloc.h>
66#include <stringops.h>
67
68 /*
69  * Global library
70  */
71#include <mail_params.h>
72
73 /*
74  * Application-specific
75  */
76#include <xsasl.h>
77#include <xsasl_cyrus.h>
78#include <xsasl_cyrus_common.h>
79
80#if defined(USE_SASL_AUTH) && defined(USE_CYRUS_SASL)
81
82#include <sasl.h>
83#include <saslutil.h>
84
85/*
86 * Silly little macros.
87 */
88#define STR(s)  vstring_str(s)
89
90 /*
91  * Macros to handle API differences between SASLv1 and SASLv2. Specifics:
92  *
93  * The SASL_LOG_* constants were renamed in SASLv2.
94  *
95  * SASLv2's sasl_client_new takes two new parameters to specify local and
96  * remote IP addresses for auth mechs that use them.
97  *
98  * SASLv2's sasl_client_start function no longer takes the secret parameter.
99  *
100  * SASLv2's sasl_decode64 function takes an extra parameter for the length of
101  * the output buffer.
102  *
103  * The other major change is that SASLv2 now takes more responsibility for
104  * deallocating memory that it allocates internally.  Thus, some of the
105  * function parameters are now 'const', to make sure we don't try to free
106  * them too.  This is dealt with in the code later on.
107  */
108#if SASL_VERSION_MAJOR < 2
109/* SASL version 1.x */
110#define SASL_CLIENT_NEW(srv, fqdn, lport, rport, prompt, secflags, pconn) \
111	sasl_client_new(srv, fqdn, prompt, secflags, pconn)
112#define SASL_CLIENT_START(conn, mechlst, secret, prompt, clout, cllen, mech) \
113	sasl_client_start(conn, mechlst, secret, prompt, clout, cllen, mech)
114#define SASL_DECODE64(in, inlen, out, outmaxlen, outlen) \
115	sasl_decode64(in, inlen, out, outlen)
116typedef char *CLIENTOUT_TYPE;
117
118#endif
119
120#if SASL_VERSION_MAJOR >= 2
121/* SASL version > 2.x */
122#define SASL_CLIENT_NEW(srv, fqdn, lport, rport, prompt, secflags, pconn) \
123	sasl_client_new(srv, fqdn, lport, rport, prompt, secflags, pconn)
124#define SASL_CLIENT_START(conn, mechlst, secret, prompt, clout, cllen, mech) \
125	sasl_client_start(conn, mechlst, prompt, clout, cllen, mech)
126#define SASL_DECODE64(in, inlen, out, outmaxlen, outlen) \
127	sasl_decode64(in, inlen, out, outmaxlen, outlen)
128typedef const char *CLIENTOUT_TYPE;
129
130#endif
131
132 /*
133  * The XSASL_CYRUS_CLIENT object is derived from the generic XSASL_CLIENT
134  * object.
135  */
136typedef struct {
137    XSASL_CLIENT xsasl;			/* generic members, must be first */
138    VSTREAM *stream;			/* client-server connection */
139    sasl_conn_t *sasl_conn;		/* SASL context */
140    VSTRING *decoded;			/* decoded server challenge */
141    sasl_callback_t *callbacks;		/* user/password lookup */
142    char   *username;
143    char   *password;
144} XSASL_CYRUS_CLIENT;
145
146 /*
147  * Forward declarations.
148  */
149static void xsasl_cyrus_client_done(XSASL_CLIENT_IMPL *);
150static XSASL_CLIENT *xsasl_cyrus_client_create(XSASL_CLIENT_IMPL *,
151					        XSASL_CLIENT_CREATE_ARGS *);
152static int xsasl_cyrus_client_set_security(XSASL_CLIENT *, const char *);
153static int xsasl_cyrus_client_first(XSASL_CLIENT *, const char *, const char *,
154			            const char *, const char **, VSTRING *);
155static int xsasl_cyrus_client_next(XSASL_CLIENT *, const char *, VSTRING *);
156static void xsasl_cyrus_client_free(XSASL_CLIENT *);
157
158/* xsasl_cyrus_client_get_user - username lookup call-back routine */
159
160static int xsasl_cyrus_client_get_user(void *context, int unused_id,
161				               const char **result,
162				               unsigned *len)
163{
164    const char *myname = "xsasl_cyrus_client_get_user";
165    XSASL_CYRUS_CLIENT *client = (XSASL_CYRUS_CLIENT *) context;
166
167    if (msg_verbose)
168	msg_info("%s: %s", myname, client->username);
169
170    /*
171     * Sanity check.
172     */
173    if (client->password == 0)
174	msg_panic("%s: no username looked up", myname);
175
176    *result = client->username;
177    if (len)
178	*len = strlen(client->username);
179    return (SASL_OK);
180}
181
182/* xsasl_cyrus_client_get_passwd - password lookup call-back routine */
183
184static int xsasl_cyrus_client_get_passwd(sasl_conn_t *conn, void *context,
185				            int id, sasl_secret_t **psecret)
186{
187    const char *myname = "xsasl_cyrus_client_get_passwd";
188    XSASL_CYRUS_CLIENT *client = (XSASL_CYRUS_CLIENT *) context;
189    int     len;
190
191    if (msg_verbose)
192	msg_info("%s: %s", myname, client->password);
193
194    /*
195     * Sanity check.
196     */
197    if (!conn || !psecret || id != SASL_CB_PASS)
198	return (SASL_BADPARAM);
199    if (client->password == 0)
200	msg_panic("%s: no password looked up", myname);
201
202    /*
203     * Convert the password into a counted string.
204     */
205    len = strlen(client->password);
206    if ((*psecret = (sasl_secret_t *) malloc(sizeof(sasl_secret_t) + len)) == 0)
207	return (SASL_NOMEM);
208    (*psecret)->len = len;
209    memcpy((*psecret)->data, client->password, len + 1);
210
211    return (SASL_OK);
212}
213
214/* xsasl_cyrus_client_init - initialize Cyrus SASL library */
215
216XSASL_CLIENT_IMPL *xsasl_cyrus_client_init(const char *unused_client_type,
217				               const char *unused_path_info)
218{
219    XSASL_CLIENT_IMPL *xp;
220    int     sasl_status;
221
222    /*
223     * Global callbacks. These have no per-session context.
224     */
225    static sasl_callback_t callbacks[] = {
226	{SASL_CB_LOG, (XSASL_CYRUS_CB) &xsasl_cyrus_log, 0},
227	{SASL_CB_LIST_END, 0, 0}
228    };
229
230#if SASL_VERSION_MAJOR >= 2 && (SASL_VERSION_MINOR >= 2 \
231    || (SASL_VERSION_MINOR == 1 && SASL_VERSION_STEP >= 19))
232    int     sasl_major;
233    int     sasl_minor;
234    int     sasl_step;
235
236    /*
237     * DLL hell guard.
238     */
239    sasl_version_info((const char **) 0, (const char **) 0,
240		      &sasl_major, &sasl_minor,
241		      &sasl_step, (int *) 0);
242    if (sasl_major != SASL_VERSION_MAJOR
243#if 0
244	|| sasl_minor != SASL_VERSION_MINOR
245	|| sasl_step != SASL_VERSION_STEP
246#endif
247	) {
248	msg_warn("incorrect SASL library version. "
249	      "Postfix was built with include files from version %d.%d.%d, "
250		 "but the run-time library version is %d.%d.%d",
251		 SASL_VERSION_MAJOR, SASL_VERSION_MINOR, SASL_VERSION_STEP,
252		 sasl_major, sasl_minor, sasl_step);
253	return (0);
254    }
255#endif
256
257    if (*var_cyrus_conf_path) {
258#ifdef SASL_PATH_TYPE_CONFIG			/* Cyrus SASL 2.1.22 */
259	if (sasl_set_path(SASL_PATH_TYPE_CONFIG,
260			  var_cyrus_conf_path) != SASL_OK)
261	    msg_warn("failed to set Cyrus SASL configuration path: \"%s\"",
262		     var_cyrus_conf_path);
263#else
264	msg_warn("%s is not empty, but setting the Cyrus SASL configuration "
265		 "path is not supported with SASL library version %d.%d.%d",
266		 VAR_CYRUS_CONF_PATH, SASL_VERSION_MAJOR,
267		 SASL_VERSION_MINOR, SASL_VERSION_STEP);
268#endif
269    }
270
271    /*
272     * Initialize the SASL library.
273     */
274    if ((sasl_status = sasl_client_init(callbacks)) != SASL_OK) {
275	msg_warn("SASL library initialization error: %s",
276		 xsasl_cyrus_strerror(sasl_status));
277	return (0);
278    }
279
280    /*
281     * Return a generic XSASL_CLIENT_IMPL object. We don't need to extend it
282     * with our own methods or data.
283     */
284    xp = (XSASL_CLIENT_IMPL *) mymalloc(sizeof(*xp));
285    xp->create = xsasl_cyrus_client_create;
286    xp->done = xsasl_cyrus_client_done;
287    return (xp);
288}
289
290/* xsasl_cyrus_client_done - dispose of implementation */
291
292static void xsasl_cyrus_client_done(XSASL_CLIENT_IMPL *impl)
293{
294    myfree((char *) impl);
295    sasl_done();
296}
297
298/* xsasl_cyrus_client_create - per-session SASL initialization */
299
300XSASL_CLIENT *xsasl_cyrus_client_create(XSASL_CLIENT_IMPL *unused_impl,
301				             XSASL_CLIENT_CREATE_ARGS *args)
302{
303    XSASL_CYRUS_CLIENT *client = 0;
304    static sasl_callback_t callbacks[] = {
305	{SASL_CB_USER, (XSASL_CYRUS_CB) &xsasl_cyrus_client_get_user, 0},
306	{SASL_CB_AUTHNAME, (XSASL_CYRUS_CB) &xsasl_cyrus_client_get_user, 0},
307	{SASL_CB_PASS, (XSASL_CYRUS_CB) &xsasl_cyrus_client_get_passwd, 0},
308	{SASL_CB_LIST_END, 0, 0}
309    };
310    sasl_conn_t *sasl_conn = 0;
311    sasl_callback_t *custom_callbacks = 0;
312    sasl_callback_t *cp;
313    int     sasl_status;
314
315    /*
316     * The optimizer will eliminate code duplication and/or dead code.
317     */
318#define XSASL_CYRUS_CLIENT_CREATE_ERROR_RETURN(x) \
319    do { \
320	if (client) { \
321	    xsasl_cyrus_client_free(&client->xsasl); \
322	} else { \
323	    if (custom_callbacks) \
324		myfree((char *) custom_callbacks); \
325	    if (sasl_conn) \
326		sasl_dispose(&sasl_conn); \
327	} \
328	return (x); \
329    } while (0)
330
331    /*
332     * Per-session initialization. Provide each session with its own callback
333     * context.
334     */
335#define NULL_SECFLAGS		0
336
337    custom_callbacks = (sasl_callback_t *) mymalloc(sizeof(callbacks));
338    memcpy((char *) custom_callbacks, callbacks, sizeof(callbacks));
339
340#define NULL_SERVER_ADDR	((char *) 0)
341#define NULL_CLIENT_ADDR	((char *) 0)
342
343    if ((sasl_status = SASL_CLIENT_NEW(args->service, args->server_name,
344				       NULL_CLIENT_ADDR, NULL_SERVER_ADDR,
345				 var_cyrus_sasl_authzid ? custom_callbacks :
346				       custom_callbacks + 1, NULL_SECFLAGS,
347				       &sasl_conn)) != SASL_OK) {
348	msg_warn("per-session SASL client initialization: %s",
349		 xsasl_cyrus_strerror(sasl_status));
350	XSASL_CYRUS_CLIENT_CREATE_ERROR_RETURN(0);
351    }
352
353    /*
354     * Extend the XSASL_CLIENT object with our own state. We use long-lived
355     * conversion buffers rather than local variables to avoid memory leaks
356     * in case of read/write timeout or I/O error.
357     *
358     * XXX If we enable SASL encryption, there needs to be a way to inform the
359     * application, so that they can turn off connection caching, refuse
360     * STARTTLS, etc.
361     */
362    client = (XSASL_CYRUS_CLIENT *) mymalloc(sizeof(*client));
363    client->xsasl.free = xsasl_cyrus_client_free;
364    client->xsasl.first = xsasl_cyrus_client_first;
365    client->xsasl.next = xsasl_cyrus_client_next;
366    client->stream = args->stream;
367    client->sasl_conn = sasl_conn;
368    client->callbacks = custom_callbacks;
369    client->decoded = vstring_alloc(20);
370    client->username = 0;
371    client->password = 0;
372
373    for (cp = custom_callbacks; cp->id != SASL_CB_LIST_END; cp++)
374	cp->context = (void *) client;
375
376    if (xsasl_cyrus_client_set_security(&client->xsasl,
377					args->security_options)
378	!= XSASL_AUTH_OK)
379	XSASL_CYRUS_CLIENT_CREATE_ERROR_RETURN(0);
380
381    return (&client->xsasl);
382}
383
384/* xsasl_cyrus_client_set_security - set security properties */
385
386static int xsasl_cyrus_client_set_security(XSASL_CLIENT *xp,
387					           const char *sasl_opts_val)
388{
389    XSASL_CYRUS_CLIENT *client = (XSASL_CYRUS_CLIENT *) xp;
390    sasl_security_properties_t sec_props;
391    int     sasl_status;
392
393    /*
394     * Per-session security properties. XXX This routine is not sufficiently
395     * documented. What is the purpose of all this?
396     */
397    memset(&sec_props, 0, sizeof(sec_props));
398    sec_props.min_ssf = 0;
399    sec_props.max_ssf = 0;			/* don't allow real SASL
400						 * security layer */
401    if (*sasl_opts_val == 0) {
402	sec_props.security_flags = 0;
403    } else {
404	sec_props.security_flags =
405	    xsasl_cyrus_security_parse_opts(sasl_opts_val);
406	if (sec_props.security_flags == 0) {
407	    msg_warn("bad per-session SASL security properties");
408	    return (XSASL_AUTH_FAIL);
409	}
410    }
411    sec_props.maxbufsize = 0;
412    sec_props.property_names = 0;
413    sec_props.property_values = 0;
414    if ((sasl_status = sasl_setprop(client->sasl_conn, SASL_SEC_PROPS,
415				    &sec_props)) != SASL_OK) {
416	msg_warn("set per-session SASL security properties: %s",
417		 xsasl_cyrus_strerror(sasl_status));
418	return (XSASL_AUTH_FAIL);
419    }
420    return (XSASL_AUTH_OK);
421}
422
423/* xsasl_cyrus_client_first - run authentication protocol */
424
425static int xsasl_cyrus_client_first(XSASL_CLIENT *xp,
426				            const char *mechanism_list,
427				            const char *username,
428				            const char *password,
429				            const char **mechanism,
430				            VSTRING *init_resp)
431{
432    const char *myname = "xsasl_cyrus_client_first";
433    XSASL_CYRUS_CLIENT *client = (XSASL_CYRUS_CLIENT *) xp;
434    unsigned enc_length;
435    unsigned enc_length_out;
436    CLIENTOUT_TYPE clientout;
437    unsigned clientoutlen;
438    int     sasl_status;
439
440#define NO_SASL_SECRET		0
441#define NO_SASL_INTERACTION	0
442
443    /*
444     * Save the username and password for the call-backs.
445     */
446    if (client->username)
447	myfree(client->username);
448    client->username = mystrdup(username);
449    if (client->password)
450	myfree(client->password);
451    client->password = mystrdup(password);
452
453    /*
454     * Start the client side authentication protocol.
455     */
456    sasl_status = SASL_CLIENT_START((sasl_conn_t *) client->sasl_conn,
457				    mechanism_list,
458				    NO_SASL_SECRET, NO_SASL_INTERACTION,
459				    &clientout, &clientoutlen, mechanism);
460    if (sasl_status != SASL_OK && sasl_status != SASL_CONTINUE) {
461	vstring_strcpy(init_resp, xsasl_cyrus_strerror(sasl_status));
462	return (XSASL_AUTH_FAIL);
463    }
464
465    /*
466     * Generate the AUTH command and the optional initial client response.
467     * sasl_encode64() produces four bytes for each complete or incomplete
468     * triple of input bytes. Allocate an extra byte for string termination.
469     */
470#define ENCODE64_LENGTH(n)	((((n) + 2) / 3) * 4)
471
472    if (clientoutlen > 0) {
473	if (msg_verbose) {
474	    escape(client->decoded, clientout, clientoutlen);
475	    msg_info("%s: uncoded initial reply: %s",
476		     myname, STR(client->decoded));
477	}
478	enc_length = ENCODE64_LENGTH(clientoutlen) + 1;
479	VSTRING_RESET(init_resp);		/* Fix 200512 */
480	VSTRING_SPACE(init_resp, enc_length);
481	if ((sasl_status = sasl_encode64(clientout, clientoutlen,
482					 STR(init_resp),
483					 vstring_avail(init_resp),
484					 &enc_length_out)) != SASL_OK)
485	    msg_panic("%s: sasl_encode64 botch: %s",
486		      myname, xsasl_cyrus_strerror(sasl_status));
487	VSTRING_AT_OFFSET(init_resp, enc_length_out);	/* XXX */
488#if SASL_VERSION_MAJOR < 2
489	/* SASL version 1 doesn't free memory that it allocates. */
490	free(clientout);
491#endif
492    } else {
493	vstring_strcpy(init_resp, "");
494    }
495    return (XSASL_AUTH_OK);
496}
497
498/* xsasl_cyrus_client_next - continue authentication */
499
500static int xsasl_cyrus_client_next(XSASL_CLIENT *xp, const char *server_reply,
501				           VSTRING *client_reply)
502{
503    const char *myname = "xsasl_cyrus_client_next";
504    XSASL_CYRUS_CLIENT *client = (XSASL_CYRUS_CLIENT *) xp;
505    unsigned enc_length;
506    unsigned enc_length_out;
507    CLIENTOUT_TYPE clientout;
508    unsigned clientoutlen;
509    unsigned serverinlen;
510    int     sasl_status;
511
512    /*
513     * Process a server challenge.
514     */
515    serverinlen = strlen(server_reply);
516    VSTRING_RESET(client->decoded);		/* Fix 200512 */
517    VSTRING_SPACE(client->decoded, serverinlen);
518    if ((sasl_status = SASL_DECODE64(server_reply, serverinlen,
519				     STR(client->decoded),
520				     vstring_avail(client->decoded),
521				     &enc_length)) != SASL_OK) {
522	vstring_strcpy(client_reply, xsasl_cyrus_strerror(sasl_status));
523	return (XSASL_AUTH_FORM);
524    }
525    if (msg_verbose)
526	msg_info("%s: decoded challenge: %.*s",
527		 myname, (int) enc_length, STR(client->decoded));
528    sasl_status = sasl_client_step(client->sasl_conn, STR(client->decoded),
529				   enc_length, NO_SASL_INTERACTION,
530				   &clientout, &clientoutlen);
531    if (sasl_status != SASL_OK && sasl_status != SASL_CONTINUE) {
532	vstring_strcpy(client_reply, xsasl_cyrus_strerror(sasl_status));
533	return (XSASL_AUTH_FAIL);
534    }
535
536    /*
537     * Send a client response.
538     */
539    if (clientoutlen > 0) {
540	if (msg_verbose)
541	    msg_info("%s: uncoded client response %.*s",
542		     myname, (int) clientoutlen, clientout);
543	enc_length = ENCODE64_LENGTH(clientoutlen) + 1;
544	VSTRING_RESET(client_reply);		/* Fix 200512 */
545	VSTRING_SPACE(client_reply, enc_length);
546	if ((sasl_status = sasl_encode64(clientout, clientoutlen,
547					 STR(client_reply),
548					 vstring_avail(client_reply),
549					 &enc_length_out)) != SASL_OK)
550	    msg_panic("%s: sasl_encode64 botch: %s",
551		      myname, xsasl_cyrus_strerror(sasl_status));
552#if SASL_VERSION_MAJOR < 2
553	/* SASL version 1 doesn't free memory that it allocates. */
554	free(clientout);
555#endif
556    } else {
557	/* XXX Can't happen. */
558	vstring_strcpy(client_reply, "");
559    }
560    return (XSASL_AUTH_OK);
561}
562
563/* xsasl_cyrus_client_free - per-session cleanup */
564
565void    xsasl_cyrus_client_free(XSASL_CLIENT *xp)
566{
567    XSASL_CYRUS_CLIENT *client = (XSASL_CYRUS_CLIENT *) xp;
568
569    if (client->username)
570	myfree(client->username);
571    if (client->password)
572	myfree(client->password);
573    if (client->sasl_conn)
574	sasl_dispose(&client->sasl_conn);
575    myfree((char *) client->callbacks);
576    vstring_free(client->decoded);
577    myfree((char *) client);
578}
579
580#endif
581