Home | History | Annotate | Line # | Download | only in smtp
      1 /*	$NetBSD: smtp_sasl_glue.c,v 1.4 2025/02/25 19:15:49 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   */
    142 static MAPS *smtp_sasl_passwd_map;
    143 
    144  /*
    145   * Supported SASL mechanisms.
    146   */
    147 STRING_LIST *smtp_sasl_mechs;
    148 
    149  /*
    150   * SASL implementation handle.
    151   */
    152 static XSASL_CLIENT_IMPL *smtp_sasl_impl;
    153 
    154  /*
    155   * The 535 SASL authentication failure cache.
    156   */
    157 #ifdef HAVE_SASL_AUTH_CACHE
    158 static SMTP_SASL_AUTH_CACHE *smtp_sasl_auth_cache;
    159 
    160 #endif
    161 
    162 /* smtp_sasl_passwd_lookup - password lookup routine */
    163 
    164 int     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 	/* Historically, the delimiter may appear in the password. */
    206 	passwd = split_at(session->sasl_username,
    207 			  *var_smtp_sasl_passwd_res_delim);
    208 	if (session->sasl_passwd)
    209 	    myfree(session->sasl_passwd);
    210 	session->sasl_passwd = mystrdup(passwd ? passwd : "");
    211 	if (msg_verbose)
    212 	    msg_info("%s: host `%s' user `%s' pass `%s'",
    213 		     myname, STR(iter->host),
    214 		     session->sasl_username, session->sasl_passwd);
    215 	return (1);
    216     } else if (smtp_sasl_passwd_map->error) {
    217 	msg_warn("%s: %s lookup error",
    218 		 state->request->queue_id, smtp_sasl_passwd_map->title);
    219 	vstream_longjmp(session->stream, SMTP_ERR_DATA);
    220     } else {
    221 	if (msg_verbose)
    222 	    msg_info("%s: no auth info found (sender=`%s', host=`%s')",
    223 		     myname, state->request->sender, STR(iter->host));
    224 	return (0);
    225     }
    226 }
    227 
    228 /* smtp_sasl_initialize - per-process initialization (pre jail) */
    229 
    230 void    smtp_sasl_initialize(void)
    231 {
    232 
    233     /*
    234      * Sanity check.
    235      */
    236     if (smtp_sasl_passwd_map || smtp_sasl_impl)
    237 	msg_panic("smtp_sasl_initialize: repeated call");
    238     if (*var_smtp_sasl_passwd == 0)
    239 	msg_fatal("specify a password table via the `%s' configuration parameter",
    240 		  VAR_LMTP_SMTP(SASL_PASSWD));
    241 
    242     /*
    243      * Open the per-host password table and initialize the SASL library. Use
    244      * shared locks for reading, just in case someone updates the table.
    245      */
    246     smtp_sasl_passwd_map = maps_create(VAR_LMTP_SMTP(SASL_PASSWD),
    247 				       var_smtp_sasl_passwd,
    248 				       DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX
    249 				       | DICT_FLAG_UTF8_REQUEST);
    250     if ((smtp_sasl_impl = xsasl_client_init(var_smtp_sasl_type,
    251 					    var_smtp_sasl_path)) == 0)
    252 	msg_fatal("SASL library initialization");
    253 
    254     /*
    255      * Initialize optional supported mechanism matchlist
    256      */
    257     if (*var_smtp_sasl_mechs)
    258 	smtp_sasl_mechs = string_list_init(VAR_SMTP_SASL_MECHS,
    259 					   MATCH_FLAG_NONE,
    260 					   var_smtp_sasl_mechs);
    261 
    262     /*
    263      * Initialize the 535 SASL authentication failure cache.
    264      */
    265     if (*var_smtp_sasl_auth_cache_name) {
    266 #ifdef HAVE_SASL_AUTH_CACHE
    267 	smtp_sasl_auth_cache =
    268 	    smtp_sasl_auth_cache_init(var_smtp_sasl_auth_cache_name,
    269 				      var_smtp_sasl_auth_cache_time);
    270 #else
    271 	msg_warn("not compiled with TLS support -- "
    272 	    "ignoring the %s setting", VAR_LMTP_SMTP(SASL_AUTH_CACHE_NAME));
    273 #endif
    274     }
    275 }
    276 
    277 /* smtp_sasl_connect - per-session client initialization */
    278 
    279 void    smtp_sasl_connect(SMTP_SESSION *session)
    280 {
    281 
    282     /*
    283      * This initialization happens whenever we instantiate an SMTP session
    284      * object. We don't instantiate a SASL client until we actually need one.
    285      */
    286     session->sasl_mechanism_list = 0;
    287     session->sasl_username = 0;
    288     session->sasl_passwd = 0;
    289     session->sasl_client = 0;
    290     session->sasl_reply = 0;
    291 }
    292 
    293 /* smtp_sasl_start - per-session SASL initialization */
    294 
    295 void    smtp_sasl_start(SMTP_SESSION *session, const char *sasl_opts_name,
    296 			        const char *sasl_opts_val)
    297 {
    298     XSASL_CLIENT_CREATE_ARGS create_args;
    299     SMTP_ITERATOR *iter = session->iterator;
    300 
    301     if (msg_verbose)
    302 	msg_info("starting new SASL client");
    303     if ((session->sasl_client =
    304 	 XSASL_CLIENT_CREATE(smtp_sasl_impl, &create_args,
    305 			     stream = session->stream,
    306 			     service = var_procname,
    307 			     server_name = STR(iter->host),
    308 			     security_options = sasl_opts_val)) == 0)
    309 	msg_fatal("SASL per-connection initialization failed");
    310     session->sasl_reply = vstring_alloc(20);
    311 }
    312 
    313 /* smtp_sasl_authenticate - run authentication protocol */
    314 
    315 int     smtp_sasl_authenticate(SMTP_SESSION *session, DSN_BUF *why)
    316 {
    317     const char *myname = "smtp_sasl_authenticate";
    318     SMTP_ITERATOR *iter = session->iterator;
    319     SMTP_RESP *resp;
    320     const char *mechanism;
    321     int     result;
    322     char   *line;
    323     int     steps = 0;
    324 
    325     /*
    326      * Sanity check.
    327      */
    328     if (session->sasl_mechanism_list == 0)
    329 	msg_panic("%s: no mechanism list", myname);
    330 
    331     if (msg_verbose)
    332 	msg_info("%s: %s: SASL mechanisms %s",
    333 		 myname, session->namaddrport, session->sasl_mechanism_list);
    334 
    335     /*
    336      * Avoid repeated login failures after a recent 535 error.
    337      */
    338 #ifdef HAVE_SASL_AUTH_CACHE
    339     if (smtp_sasl_auth_cache
    340 	&& smtp_sasl_auth_cache_find(smtp_sasl_auth_cache, session)) {
    341 	char   *resp_dsn = smtp_sasl_auth_cache_dsn(smtp_sasl_auth_cache);
    342 	char   *resp_str = smtp_sasl_auth_cache_text(smtp_sasl_auth_cache);
    343 
    344 	if (var_smtp_sasl_auth_soft_bounce && resp_dsn[0] == '5')
    345 	    resp_dsn[0] = '4';
    346 	dsb_update(why, resp_dsn, DSB_DEF_ACTION, DSB_MTYPE_DNS,
    347 		   STR(iter->host), var_procname, resp_str,
    348 		   "SASL [CACHED] authentication failed; server %s said: %s",
    349 		   STR(iter->host), resp_str);
    350 	return (0);
    351     }
    352 #endif
    353 
    354     /*
    355      * Start the client side authentication protocol.
    356      */
    357     result = xsasl_client_first(session->sasl_client,
    358 				session->sasl_mechanism_list,
    359 				session->sasl_username,
    360 				session->sasl_passwd,
    361 				&mechanism, session->sasl_reply);
    362     if (result != XSASL_AUTH_OK) {
    363 	dsb_update(why, "4.7.0", DSB_DEF_ACTION, DSB_SKIP_RMTA,
    364 		   DSB_DTYPE_SASL, STR(session->sasl_reply),
    365 		   "SASL authentication failed; "
    366 		   "cannot authenticate to server %s: %s",
    367 		   session->namaddr, STR(session->sasl_reply));
    368 	return (-1);
    369     }
    370     /*-
    371      * Send the AUTH command and the optional initial client response.
    372      *
    373      * https://tools.ietf.org/html/rfc4954#page-4
    374      * Note that the AUTH command is still subject to the line length
    375      * limitations defined in [SMTP].  If use of the initial response argument
    376      * would cause the AUTH command to exceed this length, the client MUST NOT
    377      * use the initial response parameter...
    378      *
    379      * https://tools.ietf.org/html/rfc5321#section-4.5.3.1.4
    380      * The maximum total length of a command line including the command word
    381      * and the <CRLF> is 512 octets.
    382      *
    383      * Defer the initial response if the resulting command exceeds the limit.
    384      */
    385     if (LEN(session->sasl_reply) > 0
    386 	&& strlen(mechanism) + LEN(session->sasl_reply) + 8 <= 512) {
    387 	smtp_chat_cmd(session, "AUTH %s %s", mechanism,
    388 		      STR(session->sasl_reply));
    389 	VSTRING_RESET(session->sasl_reply);	/* no deferred initial reply */
    390     } else {
    391 	smtp_chat_cmd(session, "AUTH %s", mechanism);
    392     }
    393 
    394     /*
    395      * Step through the authentication protocol until the server tells us
    396      * that we are done.  If session->sasl_reply is non-empty we have a
    397      * deferred initial reply and expect an empty initial challenge from the
    398      * server. If the server's initial challenge is non-empty we have a SASL
    399      * protocol violation with both sides wanting to go first.
    400      */
    401     while ((resp = smtp_chat_resp(session))->code / 100 == 3) {
    402 
    403 	/*
    404 	 * Sanity check.
    405 	 */
    406 	if (++steps > 100) {
    407 	    dsb_simple(why, "4.3.0", "SASL authentication failed; "
    408 		       "authentication protocol loop with server %s",
    409 		       session->namaddr);
    410 	    return (-1);
    411 	}
    412 
    413 	/*
    414 	 * Process a server challenge.
    415 	 */
    416 	line = resp->str;
    417 	(void) mystrtok(&line, "- \t\n");	/* skip over result code */
    418 
    419 	if (LEN(session->sasl_reply) > 0) {
    420 
    421 	    /*
    422 	     * Deferred initial response, the server challenge must be empty.
    423 	     * Cleared after actual transmission to the server.
    424 	     */
    425 	    if (*line) {
    426 		dsb_update(why, "4.7.0", DSB_DEF_ACTION,
    427 			   DSB_SKIP_RMTA, DSB_DTYPE_SASL, "protocol error",
    428 			   "SASL authentication failed; non-empty initial "
    429 			   "%s challenge from server %s: %s", mechanism,
    430 			   session->namaddr, STR(session->sasl_reply));
    431 		return (-1);
    432 	    }
    433 	} else {
    434 	    result = xsasl_client_next(session->sasl_client, line,
    435 				       session->sasl_reply);
    436 	    if (result != XSASL_AUTH_OK) {
    437 		dsb_update(why, "4.7.0", DSB_DEF_ACTION,	/* Fix 200512 */
    438 		    DSB_SKIP_RMTA, DSB_DTYPE_SASL, STR(session->sasl_reply),
    439 			   "SASL authentication failed; "
    440 			   "cannot authenticate to server %s: %s",
    441 			   session->namaddr, STR(session->sasl_reply));
    442 		return (-1);			/* Fix 200512 */
    443 	    }
    444 	}
    445 
    446 	/*
    447 	 * Send a client response.
    448 	 */
    449 	smtp_chat_cmd(session, "%s", STR(session->sasl_reply));
    450 	VSTRING_RESET(session->sasl_reply);	/* clear initial reply */
    451     }
    452 
    453     /*
    454      * We completed the authentication protocol.
    455      */
    456     if (resp->code / 100 != 2) {
    457 #ifdef HAVE_SASL_AUTH_CACHE
    458 	/* Update the 535 authentication failure cache. */
    459 	if (smtp_sasl_auth_cache && resp->code == 535)
    460 	    smtp_sasl_auth_cache_store(smtp_sasl_auth_cache, session, resp);
    461 #endif
    462 	if (var_smtp_sasl_auth_soft_bounce && resp->code / 100 == 5)
    463 	    STR(resp->dsn_buf)[0] = '4';
    464 	dsb_update(why, resp->dsn, DSB_DEF_ACTION,
    465 		   DSB_MTYPE_DNS, STR(iter->host),
    466 		   var_procname, resp->str,
    467 		   "SASL authentication failed; server %s said: %s",
    468 		   session->namaddr, resp->str);
    469 	return (0);
    470     }
    471     return (1);
    472 }
    473 
    474 /* smtp_sasl_cleanup - per-session cleanup */
    475 
    476 void    smtp_sasl_cleanup(SMTP_SESSION *session)
    477 {
    478     if (session->sasl_username) {
    479 	myfree(session->sasl_username);
    480 	session->sasl_username = 0;
    481     }
    482     if (session->sasl_passwd) {
    483 	myfree(session->sasl_passwd);
    484 	session->sasl_passwd = 0;
    485     }
    486     if (session->sasl_mechanism_list) {
    487 	/* allocated in smtp_sasl_helo_auth */
    488 	myfree(session->sasl_mechanism_list);
    489 	session->sasl_mechanism_list = 0;
    490     }
    491     if (session->sasl_client) {
    492 	if (msg_verbose)
    493 	    msg_info("disposing SASL state information");
    494 	xsasl_client_free(session->sasl_client);
    495 	session->sasl_client = 0;
    496     }
    497     if (session->sasl_reply) {
    498 	vstring_free(session->sasl_reply);
    499 	session->sasl_reply = 0;
    500     }
    501 }
    502 
    503 /* smtp_sasl_passivate - append serialized SASL attributes */
    504 
    505 void    smtp_sasl_passivate(SMTP_SESSION *session, VSTRING *buf)
    506 {
    507 }
    508 
    509 /* smtp_sasl_activate - de-serialize SASL attributes */
    510 
    511 int     smtp_sasl_activate(SMTP_SESSION *session, char *buf)
    512 {
    513     return (0);
    514 }
    515 
    516 #endif
    517