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