1/*	$NetBSD: smtp_sasl_glue.c,v 1.3 2020/03/18 19:05:20 christos Exp $	*/
2
3/*++
4/* NAME
5/*	smtp_sasl_glue 3
6/* SUMMARY
7/*	Postfix SASL interface for SMTP client
8/* SYNOPSIS
9/*	#include smtp_sasl.h
10/*
11/*	void	smtp_sasl_initialize()
12/*
13/*	void	smtp_sasl_connect(session)
14/*	SMTP_SESSION *session;
15/*
16/*	void	smtp_sasl_start(session, sasl_opts_name, sasl_opts_val)
17/*	SMTP_SESSION *session;
18/*
19/*	int     smtp_sasl_passwd_lookup(session)
20/*	SMTP_SESSION *session;
21/*
22/*	int	smtp_sasl_authenticate(session, why)
23/*	SMTP_SESSION *session;
24/*	DSN_BUF *why;
25/*
26/*	void	smtp_sasl_cleanup(session)
27/*	SMTP_SESSION *session;
28/*
29/*	void	smtp_sasl_passivate(session, buf)
30/*	SMTP_SESSION *session;
31/*	VSTRING	*buf;
32/*
33/*	int	smtp_sasl_activate(session, buf)
34/*	SMTP_SESSION *session;
35/*	char	*buf;
36/* DESCRIPTION
37/*	smtp_sasl_initialize() initializes the SASL library. This
38/*	routine must be called once at process startup, before any
39/*	chroot operations.
40/*
41/*	smtp_sasl_connect() performs per-session initialization. This
42/*	routine must be called once at the start of each connection.
43/*
44/*	smtp_sasl_start() performs per-session initialization. This
45/*	routine must be called once per session before doing any SASL
46/*	authentication. The sasl_opts_name and sasl_opts_val parameters are
47/*	the postfix configuration parameters setting the security
48/*	policy of the SASL authentication.
49/*
50/*	smtp_sasl_passwd_lookup() looks up the username/password
51/*	for the current SMTP server. The result is zero in case
52/*	of failure, a long jump in case of error.
53/*
54/*	smtp_sasl_authenticate() implements the SASL authentication
55/*	dialog. The result is < 0 in case of protocol failure, zero in
56/*	case of unsuccessful authentication, > 0 in case of success.
57/*	The why argument is updated with a reason for failure.
58/*	This routine must be called only when smtp_sasl_passwd_lookup()
59/*	succeeds.
60/*
61/*	smtp_sasl_cleanup() cleans up. It must be called at the
62/*	end of every SMTP session that uses SASL authentication.
63/*	This routine is a noop for non-SASL sessions.
64/*
65/*	smtp_sasl_passivate() appends flattened SASL attributes to the
66/*	specified buffer. The SASL attributes are not destroyed.
67/*
68/*	smtp_sasl_activate() restores SASL attributes from the
69/*	specified buffer. The buffer is modified. A result < 0
70/*	means there was an error.
71/*
72/*	Arguments:
73/* .IP session
74/*	Session context.
75/* .IP mech_list
76/*	String of SASL mechanisms (separated by blanks)
77/* DIAGNOSTICS
78/*	All errors are fatal.
79/* LICENSE
80/* .ad
81/* .fi
82/*	The Secure Mailer license must be distributed with this software.
83/* AUTHOR(S)
84/*	Original author:
85/*	Till Franke
86/*	SuSE Rhein/Main AG
87/*	65760 Eschborn, Germany
88/*
89/*	Adopted by:
90/*	Wietse Venema
91/*	IBM T.J. Watson Research
92/*	P.O. Box 704
93/*	Yorktown Heights, NY 10598, USA
94/*
95/*	Wietse Venema
96/*	Google, Inc.
97/*	111 8th Avenue
98/*	New York, NY 10011, USA
99/*--*/
100
101 /*
102  * System library.
103  */
104#include <sys_defs.h>
105#include <stdlib.h>
106#include <string.h>
107
108 /*
109  * Utility library
110  */
111#include <msg.h>
112#include <mymalloc.h>
113#include <stringops.h>
114#include <split_at.h>
115
116 /*
117  * Global library
118  */
119#include <mail_params.h>
120#include <string_list.h>
121#include <maps.h>
122#include <mail_addr_find.h>
123#include <smtp_stream.h>
124
125 /*
126  * XSASL library.
127  */
128#include <xsasl.h>
129
130 /*
131  * Application-specific
132  */
133#include "smtp.h"
134#include "smtp_sasl.h"
135#include "smtp_sasl_auth_cache.h"
136
137#ifdef USE_SASL_AUTH
138
139 /*
140  * Per-host login/password information.
141  */
142static MAPS *smtp_sasl_passwd_map;
143
144 /*
145  * Supported SASL mechanisms.
146  */
147STRING_LIST *smtp_sasl_mechs;
148
149 /*
150  * SASL implementation handle.
151  */
152static XSASL_CLIENT_IMPL *smtp_sasl_impl;
153
154 /*
155  * The 535 SASL authentication failure cache.
156  */
157#ifdef HAVE_SASL_AUTH_CACHE
158static SMTP_SASL_AUTH_CACHE *smtp_sasl_auth_cache;
159
160#endif
161
162/* smtp_sasl_passwd_lookup - password lookup routine */
163
164int     smtp_sasl_passwd_lookup(SMTP_SESSION *session)
165{
166    const char *myname = "smtp_sasl_passwd_lookup";
167    SMTP_STATE *state = session->state;
168    SMTP_ITERATOR *iter = session->iterator;
169    const char *value;
170    char   *passwd;
171
172    /*
173     * Sanity check.
174     */
175    if (smtp_sasl_passwd_map == 0)
176	msg_panic("%s: passwd map not initialized", myname);
177
178    /*
179     * Look up the per-server password information. Try the hostname first,
180     * then try the destination.
181     *
182     * XXX Instead of using nexthop (the intended destination) we use dest
183     * (either the intended destination, or a fall-back destination).
184     *
185     * XXX SASL authentication currently depends on the host/domain but not on
186     * the TCP port. If the port is not :25, we should append it to the table
187     * lookup key. Code for this was briefly introduced into 2.2 snapshots,
188     * but didn't canonicalize the TCP port, and did not append the port to
189     * the MX hostname.
190     */
191    smtp_sasl_passwd_map->error = 0;
192    if ((smtp_mode
193	 && var_smtp_sender_auth && state->request->sender[0]
194	 && (value = mail_addr_find(smtp_sasl_passwd_map,
195				 state->request->sender, (char **) 0)) != 0)
196	|| (smtp_sasl_passwd_map->error == 0
197	    && (value = maps_find(smtp_sasl_passwd_map,
198				  STR(iter->host), 0)) != 0)
199	|| (smtp_sasl_passwd_map->error == 0
200	    && (value = maps_find(smtp_sasl_passwd_map,
201				  STR(iter->dest), 0)) != 0)) {
202	if (session->sasl_username)
203	    myfree(session->sasl_username);
204	session->sasl_username = mystrdup(value);
205	passwd = split_at(session->sasl_username, ':');
206	if (session->sasl_passwd)
207	    myfree(session->sasl_passwd);
208	session->sasl_passwd = mystrdup(passwd ? passwd : "");
209	if (msg_verbose)
210	    msg_info("%s: host `%s' user `%s' pass `%s'",
211		     myname, STR(iter->host),
212		     session->sasl_username, session->sasl_passwd);
213	return (1);
214    } else if (smtp_sasl_passwd_map->error) {
215	msg_warn("%s: %s lookup error",
216		 state->request->queue_id, smtp_sasl_passwd_map->title);
217	vstream_longjmp(session->stream, SMTP_ERR_DATA);
218    } else {
219	if (msg_verbose)
220	    msg_info("%s: no auth info found (sender=`%s', host=`%s')",
221		     myname, state->request->sender, STR(iter->host));
222	return (0);
223    }
224}
225
226/* smtp_sasl_initialize - per-process initialization (pre jail) */
227
228void    smtp_sasl_initialize(void)
229{
230
231    /*
232     * Sanity check.
233     */
234    if (smtp_sasl_passwd_map || smtp_sasl_impl)
235	msg_panic("smtp_sasl_initialize: repeated call");
236    if (*var_smtp_sasl_passwd == 0)
237	msg_fatal("specify a password table via the `%s' configuration parameter",
238		  VAR_LMTP_SMTP(SASL_PASSWD));
239
240    /*
241     * Open the per-host password table and initialize the SASL library. Use
242     * shared locks for reading, just in case someone updates the table.
243     */
244    smtp_sasl_passwd_map = maps_create(VAR_LMTP_SMTP(SASL_PASSWD),
245				       var_smtp_sasl_passwd,
246				       DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX
247				       | DICT_FLAG_UTF8_REQUEST);
248    if ((smtp_sasl_impl = xsasl_client_init(var_smtp_sasl_type,
249					    var_smtp_sasl_path)) == 0)
250	msg_fatal("SASL library initialization");
251
252    /*
253     * Initialize optional supported mechanism matchlist
254     */
255    if (*var_smtp_sasl_mechs)
256	smtp_sasl_mechs = string_list_init(VAR_SMTP_SASL_MECHS,
257					   MATCH_FLAG_NONE,
258					   var_smtp_sasl_mechs);
259
260    /*
261     * Initialize the 535 SASL authentication failure cache.
262     */
263    if (*var_smtp_sasl_auth_cache_name) {
264#ifdef HAVE_SASL_AUTH_CACHE
265	smtp_sasl_auth_cache =
266	    smtp_sasl_auth_cache_init(var_smtp_sasl_auth_cache_name,
267				      var_smtp_sasl_auth_cache_time);
268#else
269	msg_warn("not compiled with TLS support -- "
270	    "ignoring the %s setting", VAR_LMTP_SMTP(SASL_AUTH_CACHE_NAME));
271#endif
272    }
273}
274
275/* smtp_sasl_connect - per-session client initialization */
276
277void    smtp_sasl_connect(SMTP_SESSION *session)
278{
279
280    /*
281     * This initialization happens whenever we instantiate an SMTP session
282     * object. We don't instantiate a SASL client until we actually need one.
283     */
284    session->sasl_mechanism_list = 0;
285    session->sasl_username = 0;
286    session->sasl_passwd = 0;
287    session->sasl_client = 0;
288    session->sasl_reply = 0;
289}
290
291/* smtp_sasl_start - per-session SASL initialization */
292
293void    smtp_sasl_start(SMTP_SESSION *session, const char *sasl_opts_name,
294			        const char *sasl_opts_val)
295{
296    XSASL_CLIENT_CREATE_ARGS create_args;
297    SMTP_ITERATOR *iter = session->iterator;
298
299    if (msg_verbose)
300	msg_info("starting new SASL client");
301    if ((session->sasl_client =
302	 XSASL_CLIENT_CREATE(smtp_sasl_impl, &create_args,
303			     stream = session->stream,
304			     service = var_procname,
305			     server_name = STR(iter->host),
306			     security_options = sasl_opts_val)) == 0)
307	msg_fatal("SASL per-connection initialization failed");
308    session->sasl_reply = vstring_alloc(20);
309}
310
311/* smtp_sasl_authenticate - run authentication protocol */
312
313int     smtp_sasl_authenticate(SMTP_SESSION *session, DSN_BUF *why)
314{
315    const char *myname = "smtp_sasl_authenticate";
316    SMTP_ITERATOR *iter = session->iterator;
317    SMTP_RESP *resp;
318    const char *mechanism;
319    int     result;
320    char   *line;
321    int     steps = 0;
322
323    /*
324     * Sanity check.
325     */
326    if (session->sasl_mechanism_list == 0)
327	msg_panic("%s: no mechanism list", myname);
328
329    if (msg_verbose)
330	msg_info("%s: %s: SASL mechanisms %s",
331		 myname, session->namaddrport, session->sasl_mechanism_list);
332
333    /*
334     * Avoid repeated login failures after a recent 535 error.
335     */
336#ifdef HAVE_SASL_AUTH_CACHE
337    if (smtp_sasl_auth_cache
338	&& smtp_sasl_auth_cache_find(smtp_sasl_auth_cache, session)) {
339	char   *resp_dsn = smtp_sasl_auth_cache_dsn(smtp_sasl_auth_cache);
340	char   *resp_str = smtp_sasl_auth_cache_text(smtp_sasl_auth_cache);
341
342	if (var_smtp_sasl_auth_soft_bounce && resp_dsn[0] == '5')
343	    resp_dsn[0] = '4';
344	dsb_update(why, resp_dsn, DSB_DEF_ACTION, DSB_MTYPE_DNS,
345		   STR(iter->host), var_procname, resp_str,
346		   "SASL [CACHED] authentication failed; server %s said: %s",
347		   STR(iter->host), resp_str);
348	return (0);
349    }
350#endif
351
352    /*
353     * Start the client side authentication protocol.
354     */
355    result = xsasl_client_first(session->sasl_client,
356				session->sasl_mechanism_list,
357				session->sasl_username,
358				session->sasl_passwd,
359				&mechanism, session->sasl_reply);
360    if (result != XSASL_AUTH_OK) {
361	dsb_update(why, "4.7.0", DSB_DEF_ACTION, DSB_SKIP_RMTA,
362		   DSB_DTYPE_SASL, STR(session->sasl_reply),
363		   "SASL authentication failed; "
364		   "cannot authenticate to server %s: %s",
365		   session->namaddr, STR(session->sasl_reply));
366	return (-1);
367    }
368    /*-
369     * Send the AUTH command and the optional initial client response.
370     *
371     * https://tools.ietf.org/html/rfc4954#page-4
372     * Note that the AUTH command is still subject to the line length
373     * limitations defined in [SMTP].  If use of the initial response argument
374     * would cause the AUTH command to exceed this length, the client MUST NOT
375     * use the initial response parameter...
376     *
377     * https://tools.ietf.org/html/rfc5321#section-4.5.3.1.4
378     * The maximum total length of a command line including the command word
379     * and the <CRLF> is 512 octets.
380     *
381     * Defer the initial response if the resulting command exceeds the limit.
382     */
383    if (LEN(session->sasl_reply) > 0
384	&& strlen(mechanism) + LEN(session->sasl_reply) + 8 <= 512) {
385	smtp_chat_cmd(session, "AUTH %s %s", mechanism,
386		      STR(session->sasl_reply));
387	VSTRING_RESET(session->sasl_reply);	/* no deferred initial reply */
388    } else {
389	smtp_chat_cmd(session, "AUTH %s", mechanism);
390    }
391
392    /*
393     * Step through the authentication protocol until the server tells us
394     * that we are done.  If session->sasl_reply is non-empty we have a
395     * deferred initial reply and expect an empty initial challenge from the
396     * server. If the server's initial challenge is non-empty we have a SASL
397     * protocol violation with both sides wanting to go first.
398     */
399    while ((resp = smtp_chat_resp(session))->code / 100 == 3) {
400
401	/*
402	 * Sanity check.
403	 */
404	if (++steps > 100) {
405	    dsb_simple(why, "4.3.0", "SASL authentication failed; "
406		       "authentication protocol loop with server %s",
407		       session->namaddr);
408	    return (-1);
409	}
410
411	/*
412	 * Process a server challenge.
413	 */
414	line = resp->str;
415	(void) mystrtok(&line, "- \t\n");	/* skip over result code */
416
417	if (LEN(session->sasl_reply) > 0) {
418
419	    /*
420	     * Deferred initial response, the server challenge must be empty.
421	     * Cleared after actual transmission to the server.
422	     */
423	    if (*line) {
424		dsb_update(why, "4.7.0", DSB_DEF_ACTION,
425			   DSB_SKIP_RMTA, DSB_DTYPE_SASL, "protocol error",
426			   "SASL authentication failed; non-empty initial "
427			   "%s challenge from server %s: %s", mechanism,
428			   session->namaddr, STR(session->sasl_reply));
429		return (-1);
430	    }
431	} else {
432	    result = xsasl_client_next(session->sasl_client, line,
433				       session->sasl_reply);
434	    if (result != XSASL_AUTH_OK) {
435		dsb_update(why, "4.7.0", DSB_DEF_ACTION,	/* Fix 200512 */
436		    DSB_SKIP_RMTA, DSB_DTYPE_SASL, STR(session->sasl_reply),
437			   "SASL authentication failed; "
438			   "cannot authenticate to server %s: %s",
439			   session->namaddr, STR(session->sasl_reply));
440		return (-1);			/* Fix 200512 */
441	    }
442	}
443
444	/*
445	 * Send a client response.
446	 */
447	smtp_chat_cmd(session, "%s", STR(session->sasl_reply));
448	VSTRING_RESET(session->sasl_reply);	/* clear initial reply */
449    }
450
451    /*
452     * We completed the authentication protocol.
453     */
454    if (resp->code / 100 != 2) {
455#ifdef HAVE_SASL_AUTH_CACHE
456	/* Update the 535 authentication failure cache. */
457	if (smtp_sasl_auth_cache && resp->code == 535)
458	    smtp_sasl_auth_cache_store(smtp_sasl_auth_cache, session, resp);
459#endif
460	if (var_smtp_sasl_auth_soft_bounce && resp->code / 100 == 5)
461	    STR(resp->dsn_buf)[0] = '4';
462	dsb_update(why, resp->dsn, DSB_DEF_ACTION,
463		   DSB_MTYPE_DNS, STR(iter->host),
464		   var_procname, resp->str,
465		   "SASL authentication failed; server %s said: %s",
466		   session->namaddr, resp->str);
467	return (0);
468    }
469    return (1);
470}
471
472/* smtp_sasl_cleanup - per-session cleanup */
473
474void    smtp_sasl_cleanup(SMTP_SESSION *session)
475{
476    if (session->sasl_username) {
477	myfree(session->sasl_username);
478	session->sasl_username = 0;
479    }
480    if (session->sasl_passwd) {
481	myfree(session->sasl_passwd);
482	session->sasl_passwd = 0;
483    }
484    if (session->sasl_mechanism_list) {
485	/* allocated in smtp_sasl_helo_auth */
486	myfree(session->sasl_mechanism_list);
487	session->sasl_mechanism_list = 0;
488    }
489    if (session->sasl_client) {
490	if (msg_verbose)
491	    msg_info("disposing SASL state information");
492	xsasl_client_free(session->sasl_client);
493	session->sasl_client = 0;
494    }
495    if (session->sasl_reply) {
496	vstring_free(session->sasl_reply);
497	session->sasl_reply = 0;
498    }
499}
500
501/* smtp_sasl_passivate - append serialized SASL attributes */
502
503void    smtp_sasl_passivate(SMTP_SESSION *session, VSTRING *buf)
504{
505}
506
507/* smtp_sasl_activate - de-serialize SASL attributes */
508
509int     smtp_sasl_activate(SMTP_SESSION *session, char *buf)
510{
511    return (0);
512}
513
514#endif
515