/* * ZMailer smtpserver, AUTH command things (RFC 2554, sort of); * part of ZMailer. * * by Matti Aarnio 1999,2002,2003 * * The basis of SASL[2] code is from Sendmail 8.12.3 * */ #include "smtpserver.h" int SASLSecOpts; const char *SASL_Auth_Mechanisms; /* "AUTH LOGIN" command per http://help.netscape.com/products/server/messaging/3x/info/smtpauth.html Authenticated SMTP When the client submits a message to the server using SMTP, the server supports an SMTP Service Extension for Authentication (SMTP Authentication), as proposed by John Myers as part of the Simple Authentication and Security Layer [SASL]. Specifically, the Netscape Messaging products support the "AUTH LOGIN" extension, which uses a base64 encoding of username and password. While this is a very simplistic authentication method that is little better than cleartext passwords, it is supported by a number of other messaging programs (including Sun's Solstice IMAP products, the UW IMAP server, NetManage's IMAP client, Sun's java IMAP client.) In future messaging products, Netscape plans to support stronger authentication methods such as pklogin. Here is a sample of the dialog between client and server: S: 220 jimi-hendrix.mcom.com ESMTP server (Netscape Messaging Server - Version 3.0) ready Fri, 2 May 1997 09:38:41 -0700 C: ehlo jimi S: 250-jimi-hendrix.mcom.com S: 250-HELP S: 250-ETRN S: 250-PIPELINING S: 250-DSN S: 250 AUTH=LOGIN C: auth login S: 334 VXNlcm5hbWU6 base64 "Username:" C: bXluYW1l base64 "myname" S: 334 Uc2VjcmV0 base64 "Password:" C: GFzc3dvcmQ6 base64 "secret" S: 235 Authentication successful For server exchanges at this point, the sending server (C:) would put: C: MAIL FROM: AUTH= and receiving server (S:) would reply with: S: 250 Sender and extensions (AUTH=) Ok */ extern char * zpwmatch __((char *, char *, long *uidp)); extern char * pipezpwmatch __((char *, char *, char *, long *uidp)); #if 0 /* DUMMY BEAST... */ /* This is *NOT* universal password matcher! Consider Shadow passwords, PAM systems, etc.. */ #include #include /* Return NULL for OK, and error text for failure */ char * zpwmatch(uname,password,uidp) char *uname, *password; long *uidp; { struct Zpasswd *pw = zgetpwnam(uname); /* ... */ char *cr; if (!pw) return -1; /* No such user */ cr = crypt(password, pw->pw_passwd); *uidp = pw->pw_uid; return (strcmp(cr, pw->pw_passwd) == 0) ? NULL : "Authentication Failure"; } #endif #ifdef HAVE_SASL2 /* ** ITEMINLIST -- does item appear in list? ** ** Check whether item appears in list (which must be separated by a ** character in delim) as a "word", i.e. it must appear at the begin ** of the list or after a space, and it must end with a space or the ** end of the list. ** ** Parameters: ** item -- item to search. ** list -- list of items. ** delim -- list of delimiters. ** ** Returns: ** pointer to occurrence (NULL if not found). */ static const char * iteminlist __((const char *, const char *, const char *)); static const char * iteminlist(item, list, delim) const char *item; const char *list; const char *delim; { const char *s; int len; if (list == NULL || *list == '\0') return NULL; if (item == NULL || *item == '\0') return NULL; s = list; len = strlen(item); while (s != NULL && *s != '\0') { if (strncasecmp(s, item, len) == 0 && (s[len] == '\0' || strchr(delim, s[len]) != NULL)) return s; s = strpbrk(s, delim); if (s != NULL) while (*++s == ' ') continue; } return NULL; } /* ** INTERSECT -- create the intersection between two lists ** ** Parameters: ** s1, s2 -- lists of items (separated by single blanks). ** rpool -- resource pool from which result is allocated. ** ** Returns: ** the intersection of both lists. */ static const char * intersect __((const char *, const char *)); static const char * intersect(s1, s2) const char *s1, *s2; { char *hr, *h1, *h, *res; int l1, l2, rl; if (s1 == NULL || s2 == NULL) /* NULL string(s) -> NULL result */ return NULL; l1 = strlen(s1); l2 = strlen(s2); rl = (l1 < l2 ? l1 : l2); res = (char *) malloc(rl + 1); if (res == NULL) return NULL; *res = '\0'; if (rl == 0) /* at least one string empty? */ return res; hr = res; h1 = (char *) s1; h = (char *) s1; /* walk through s1 */ while (h != NULL && *h1 != '\0') { /* is there something after the current word? */ if ((h = strchr(h1, ' ')) != NULL) *h = '\0'; l1 = strlen(h1); /* does the current word appear in s2 ? */ if (iteminlist(h1, s2, " ") != NULL) { /* add a blank if not first item */ if (hr != res) *hr++ = ' '; /* copy the item */ memcpy(hr, h1, l1); /* advance pointer in result list */ hr += l1; *hr = '\0'; } if (h != NULL) { /* there are more items */ *h = ' '; h1 = h + 1; } } return res; } #endif void smtp_auth(SS,buf,cp) SmtpState * SS; const char *buf; const char *cp; { char abuf[SMTPLINESIZE]; /* limits size of SMTP commands... On the other hand, limit is asked to be only 1000 chars, not 8k.. */ char bbuf[SMTPLINESIZE]; char c, co; int i, rc; char *uname; long uid; char *zpw; if (SS->authuser != NULL) { type(SS, 503, m551, "Already authenticated, second attempt rejected!"); return; } if (SS->state == Hello) { type(SS, 503, m551, "EHLO first, then - perhaps - AUTH!"); return; } if (SS->state != MailOrHello && SS->state != Mail) { type(SS, 503, m551, "AUTH not allowed during MAIL transaction!"); return; } if (*cp == ' ') ++cp; if (!strict_protocol) while (*cp == ' ' || *cp == '\t') ++cp; #ifdef HAVE_SASL2 if (!do_sasl) #endif { if (!CISTREQN(cp, "LOGIN", 5)) { type(SS, 504, m571, "Only 'AUTH LOGIN' supported."); return; } #ifdef HAVE_OPENSSL if (!auth_login_without_tls && !SS->sslmode) { type(SS, 503, m571, "Plaintext password authentication must be run under SSL/TLS"); return; } #endif /* - HAVE_OPENSSL */ #ifndef HAVE_OPENSSL if (!auth_login_without_tls) { type(SS, 503, m571, "Plaintext password authentication is not enabled in this system"); return; } #endif /* --HAVE_OPENSSL */ cp += 5; if (*cp == ' ') ++cp; if (!strict_protocol) while (*cp == ' ' || *cp == '\t') ++cp; if (*cp != 0) { const char *ccp; rc = decodebase64string(cp, strlen(cp), bbuf, sizeof(bbuf), &ccp); bbuf[sizeof(bbuf)-1] = 0; if (debug) type(SS, 0, NULL, "-> %s", bbuf); uname = strdup(bbuf); if (*ccp != 0) { type(SS, 501, m552, "unrecognized input/extra junk ??"); return; } } else { i = encodebase64string("Username:", 9, abuf, sizeof(abuf)); if (i >= sizeof(abuf)) i = sizeof(abuf)-1; abuf[i] = 0; type(SS, 334, NULL, "%s", abuf); i = s_gets(SS, abuf, sizeof(abuf), &rc, &co, &c ); abuf[sizeof(abuf)-1] = 0; if (logfp_to_syslog || logfp) time( & now ); if (logfp_to_syslog) zsyslog((LOG_DEBUG, "%s%04d r %s", logtag, (int)(now - logtagepoch), abuf)); if (logfp != NULL) { fprintf(logfp, "%s%04dr\t%s\n", logtag, (int)(now - logtagepoch), abuf); fflush(logfp); } if (i == 0) /* EOF ??? */ return; if (strcmp(abuf, "*") == 0) { type(SS, 501, NULL, "AUTH command cancelled"); return; } rc = decodebase64string(abuf, i, bbuf, sizeof(bbuf), NULL); bbuf[sizeof(bbuf)-1] = 0; if (debug) type(SS, 0, NULL, "-> %s", bbuf); uname = strdup(bbuf); } i = encodebase64string("Password:", 9, abuf, sizeof(abuf)); if (i >= sizeof(abuf)) i = sizeof(abuf)-1; abuf[i] = 0; type(SS, 334, NULL, "%s", abuf); i = s_gets(SS, abuf, sizeof(abuf), &rc, &co, &c ); abuf[sizeof(abuf)-1] = 0; if (logfp_to_syslog || logfp) time( & now ); #if 0 /* This logs encoded password, usually that is *not* desired */ if (logfp_to_syslog) zsyslog((LOG_DEBUG, "%s%04d r %s", logtag, (int)(now - logtagepoch), abuf)); if (logfp != NULL) { fprintf(logfp, "%s%04dr\t%s\n", logtag, (int)(now - logtagepoch), abuf); fflush(logfp); } #else if (logfp_to_syslog) zsyslog((LOG_DEBUG, "%s%04d r **base64-password**", logtag, (int)(now - logtagepoch) )); if (logfp != NULL) { fprintf(logfp, "%s%04dr\t**base64-password**\n", logtag, (int)(now - logtagepoch) ); fflush(logfp); } #endif if (i == 0) { /* EOF ??? */ if (uname) free(uname); return; } if (strcmp(abuf, "*") == 0) { if (uname) free(uname); type(SS, 501, NULL, "AUTH command cancelled"); return; } rc = decodebase64string(abuf, i, bbuf, sizeof(bbuf), NULL); bbuf[sizeof(bbuf)-1] = 0; if (debug) type(SS, 0, NULL, "-> %s", bbuf); if (tls_loglevel > 3) type(NULL,0,NULL,"zpwmatch: user ´%s' password '%s'", uname, bbuf); else if (tls_loglevel > 0) type(NULL,0,NULL,"zpwmatch: user ´%s' (password: *not so easy*!)", uname); if (smtpauth_via_pipe) zpw = pipezpwmatch(smtpauth_via_pipe, uname, bbuf, &uid); else zpw = zpwmatch(uname, bbuf, &uid); if (zpw == NULL) { SS->authuser = uname; type(SS, 235, NULL, "Authentication successful."); } else { type(SS, 535, NULL, "%s", zpw); if (uname) free(uname); } } #ifdef HAVE_SASL2 else { /* Here we support CMU Cyrus-SASL-2 server side code. Unlike sendmail, we keep state here by spinning around where necessary.. */ #define SASL_NOT_AUTH 0 #define SASL_PROC_AUTH 1 #define SASL_IS_AUTH 2 int authenticating = SASL_PROC_AUTH; int ismore = 0; char *q, *in, *out, *out2; int len, inlen, outlen, out2len; int result; char *auth_type; /* make sure mechanism (p) is a valid string */ for (q = (char*)cp; *q != '\0' && isascii(*q); q++) { if (isspace(*q)) { *q = '\0'; while (*++q != '\0' && isascii(*q) && isspace(*q)) continue; *(q - 1) = '\0'; ismore = (*q != '\0'); break; } } /* check whether mechanism is available */ if (iteminlist(cp, SS->sasl.mechlist, " ") == NULL) { type(SS, 503, "5.3.3", "AUTH mechanism %.32s not available", cp); return; } if (ismore) { /* could this be shorter? XXX */ len = 1+strlen(q); in = malloc(len); result = sasl_decode64(q, len-1, in, len, &inlen); if (result != SASL_OK) { type(SS, 501, "5.5.4", "cannot BASE64 decode '%s'", q); authenticating = SASL_NOT_AUTH; free(in); return; } } else { in = NULL; inlen = 0; } auth_type = strdup(cp); /* see if that auth type exists */ result = sasl_server_start(SS->sasl.conn, auth_type, in, (unsigned) inlen, (const char **) & out, (unsigned*) & outlen); if (result != SASL_OK && result != SASL_CONTINUE) { type(SS, 500, "5.7.0", "authentication failed"); if (logfp){ const char * e = sasl_errdetail(SS->sasl.conn); if (!e) e = "<-no-detail->"; fprintf(logfp, "%s%04d#\tAUTH failure (%s): %s (%d) %s\n", logtag, (int)(now - logtagepoch), cp, sasl_errstring(result, NULL, NULL), result, e); fflush(logfp); } return; } if (result == SASL_OK) { /* ugly, but same code */ goto authenticated; /* authenticated by the initial response */ } /* len is at least 2 */ len = (4*outlen)/3+2; out2 = malloc(len); result = sasl_encode64(out, outlen, out2, len, &out2len); if (result != SASL_OK) { type(SS, 454, "4.5.4", "Temporary authentication failure"); if (logfp) { fprintf(logfp, "%s%d#\tAUTH encode64 error [%d for \"%s\"]\n", logtag, (int)(now - logtagepoch), result, out); fflush(logfp); } /* start over? */ authenticating = SASL_NOT_AUTH; } else { type(SS, 334, "", "%s", out2); authenticating = SASL_PROC_AUTH; } while (authenticating == SASL_PROC_AUTH) { i = s_gets( SS, abuf, sizeof(abuf), &rc, &co, &c ); abuf[sizeof(abuf)-1] = 0; if (i >= sizeof(abuf)) i = sizeof(abuf)-1; if (logfp != NULL) { #if 1 fprintf(logfp, "%s%04dr\t**user-response** -- len=%d\n", logtag, (int)(now - logtagepoch), i ); #else fprintf(logfp, "%s%04dr\t%s\n", logtag, (int)(now - logtagepoch), abuf ); #endif fflush(logfp); } if (abuf[0] == '\0' || i == 0) { authenticating = SASL_NOT_AUTH; type(SS, 501, "5.5.2", "missing input"); break; } if (abuf[0] == '*' && abuf[1] == '\0') { authenticating = SASL_NOT_AUTH; /* rfc 2254 4. */ type(SS, 501, "5.0.0", "AUTH aborted"); break; } /* could this be shorter? XXX */ result = sasl_decode64(abuf, i, bbuf, sizeof(bbuf), &outlen); if (result != SASL_OK) { authenticating = SASL_NOT_AUTH; /* rfc 2254 4. */ type(SS, 501, "5.5.4", "cannot decode AUTH parameter %s", abuf); continue; } result = sasl_server_step(SS->sasl.conn, (const char *) bbuf, (unsigned) outlen, (const char **) & out2, (unsigned *) & out2len ); /* get an OK if we're done */ if (result == SASL_OK) { authenticated: type(SS, 235, m200, "OK Authenticated"); authenticating = SASL_IS_AUTH; /* macdefine(&BlankEnvelope.e_macro, A_TEMP, macid("{auth_type}"), auth_type); */ result = sasl_getprop(SS->sasl.conn, SASL_USERNAME, (const void **)&SS->authuser); /* XX: check result == SASL_OK ?? */ # if 0 /* get realm? */ sasl_getprop(SS->sasl.conn, SASL_REALM, (const void **) &data); # endif /* 0 */ /* get security strength (features) */ result = sasl_getprop(SS->sasl.conn, SASL_SSF, (const void **) &SS->sasl.ssf); #if 0 if (result == SASL_OK) { char pbuf[8]; (void) sm_snprintf(pbuf, sizeof pbuf, "%u", *ssf); macdefine(&BlankEnvelope.e_macro, A_TEMP, macid("{auth_ssf}"), pbuf); if (tTd(95, 8)) sm_dprintf("AUTH auth_ssf: %u\n", *ssf); } /* ** Only switch to encrypted connection ** if a security layer has been negotiated */ if (SS->sasl.ssf != NULL && SS->sasl.ssf[0] > 0) { /* ** Convert I/O layer to use SASL. ** If the call fails, the connection ** is aborted. */ if (sfdcsasl(&InChannel, &OutChannel, conn) == 0) { /* restart dialogue */ n_helo = 0; # if PIPELINING (void) sm_io_autoflush(InChannel, OutChannel); # endif /* PIPELINING */ } else syserr("503 5.3.3 SASL TLS failed"); } #endif #if 0 /* NULL pointer ok since it's our function */ if (LogLevel > 8) sm_syslog(LOG_INFO, NOQID, "AUTH=server, relay=%.100s, authid=%.128s, mech=%.16s, bits=%d", CurSmtpClient, shortenstring(user, 128), auth_type, *ssf); #endif } else if (result == SASL_CONTINUE) { len = (4*outlen)/3+2; out2 = malloc(len); result = sasl_encode64(out, outlen, out2, len, &out2len); if (result != SASL_OK) { /* correct code? XXX */ /* 454 Temp. authentication failure */ type(SS, 454, "4.5.4", "Internal error: unable to encode64"); #if 0 if (LogLevel > 5) sm_syslog(LOG_WARNING, e->e_id, "AUTH encode64 error [%d for \"%s\"]", result, out); #endif /* start over? */ authenticating = SASL_NOT_AUTH; } else { type(SS, 334, "", "%s", out2); #if 0 if (tTd(95, 2)) sm_dprintf("AUTH continue: msg='%s' len=%u\n", out2, out2len); #endif } } else { /* not SASL_OK or SASL_CONT */ type(SS, 500, "5.7.0", "authentication failed"); #if 0 if (LogLevel > 9) sm_syslog(LOG_WARNING, e->e_id, "AUTH failure (%s): %s (%d) %s", auth_type, sasl_errstring(result, NULL, NULL), result, errstr == NULL ? "" : errstr); #endif authenticating = SASL_NOT_AUTH; } } exit_cleanup: free(auth_type); if (in) free(in); } #endif /* HAVE_SASL2 */ } #ifdef HAVE_SASL2 /* ** PROXY_POLICY -- define proxy policy for AUTH ** ** Parameters: ** context -- unused. ** auth_identity -- authentication identity. ** requested_user -- authorization identity. ** user -- allowed user (output). ** errstr -- possible error string (output). ** ** Returns: ** ok? */ int proxy_policy(context, auth_identity, requested_user, user, errstr) void *context; const char *auth_identity; const char *requested_user; const char **user; const char **errstr; { if (user == NULL || auth_identity == NULL) return SASL_FAIL; *user = strdup(auth_identity); return SASL_OK; } #if 1 static sasl_callback_t srvcallbacks[] = { { SASL_CB_PROXY_POLICY, &proxy_policy, NULL }, { SASL_CB_LIST_END, NULL, NULL } }; #else static sasl_callback_t srvcallbacks[] = { { SASL_CB_VERIFYFILE, &safesaslfile, NULL }, { SASL_CB_PROXY_POLICY, &proxy_policy, NULL }, { SASL_CB_LIST_END, NULL, NULL } }; #endif #endif /* HAVE_SASL2 */ void smtpauth_init(SS) SmtpState *SS; { #ifdef HAVE_SASL2 int result; if (do_sasl) { SS->sasl.n_mechs = 0; /* SASL server new connection */ result = sasl_server_init(srvcallbacks, "smtpserver"); SS->sasl.sasl_ok = (result == SASL_OK); if (result != SASL_OK) type(NULL,0,NULL, "sasl_server_init() failed; result=%d", result); if (SS->sasl.sasl_ok) { /* use empty realm: only works in SASL > 1.5.5 */ /* Will it works with SASL 2.x ? */ result = sasl_server_new("smtpserver", /* service */ SS->myhostname, /* serverFQDN */ "", /* user_realm */ NULL, /* iplocalport literal */ NULL, /* ipremoteport literal */ NULL, /* callbacks */ 0, /* flags */ &SS->sasl.conn); SS->sasl.sasl_ok = (result == SASL_OK); if (result != SASL_OK) type(NULL,0,NULL, "sasl_server_new() failed; result=%d", result); } #ifdef SASL_IP_REMOTE /* Cyrus Sasl 1.x, only. Not in 2.x */ if (SS->sasl.sasl_ok) { /* ** SASL set properties for sasl ** set local/remote IP ** XXX only IPv4: Cyrus SASL doesn't support anything else ** ** XXX where exactly are these used/required? ** Kerberos_v4 */ sasl_setprop(SS->sasl.conn, SASL_IP_REMOTE, &SS->raddr); sasl_setprop(SS->sasl.conn, SASL_IP_LOCAL, &SS->localsock); } #endif SS->sasl.auth_type = NULL; SS->sasl.mechlist = NULL; /* clear sasl security properties */ (void) memset(&SS->sasl.ssp, 0, sizeof(SS->sasl.ssp)); /* XXX should these be options settable via .cf ? */ /* ssp.min_ssf = 0; is default due to memset() */ # if STARTTLS # endif /* STARTTLS */ SS->sasl.ssp.max_ssf = 0; /* XX: no security-strength-factor supported! */ SS->sasl.ssp.maxbufsize = 1024; /* MAGIC! */ } #endif } void smtpauth_ehloresponse(SS) SmtpState *SS; { int result; #ifdef HAVE_SASL2 if (do_sasl) { if (SS->sasl.sasl_ok) { SS->sasl.ssp.security_flags = (SASLSecOpts & SASL_SEC_MAXIMUM); if (!(auth_login_without_tls || SS->sslmode)) { SS->sasl.ssp.security_flags |= SASL_SEC_NOPLAINTEXT; } SS->sasl.ssp.security_flags |= SASL_SEC_NOANONYMOUS; result = sasl_setprop(SS->sasl.conn, SASL_SEC_PROPS, &SS->sasl.ssp); SS->sasl.sasl_ok = (result == SASL_OK); #if 0 /* Is in SASL-1, different/not in SASL-2 */ #ifdef SASL_SSF_EXTERNAL if (SS->sasl.sasl_ok) { /* ** external security strength factor; ** currently we have none so zero */ SS->sasl.ext_ssf.ssf = 0; SS->sasl.ext_ssf.auth_id = NULL; result = sasl_setprop(SS->sasl.conn, SASL_SSF_EXTERNAL, &SS->sasl.ext_ssf); SS->sasl.sasl_ok = (result == SASL_OK); } #endif #endif } if (SS->sasl.sasl_ok) { int len, num; /* "user" is currently unused */ result = sasl_listmech(SS->sasl.conn, "user", /* XXX */ "", " ", "", &SS->sasl.mechlist, (unsigned int *)&len, (unsigned int *)&num); if (result != SASL_OK) { type(NULL,0,NULL, "AUTH error: listmech=%d, num=%d", result, num); num = 0; } if (num > 0) { type(NULL,0,NULL, "AUTH: available mech=%s, allowed mech=%s", SS->sasl.mechlist, SASL_Auth_Mechanisms ? SASL_Auth_Mechanisms : "<-nil->" ); if (SASL_Auth_Mechanisms) SS->sasl.mechlist = intersect( SASL_Auth_Mechanisms, SS->sasl.mechlist ); } else { SS->sasl.mechlist = NULL; /* be paranoid... */ type(NULL,0,NULL, "AUTH warning: no mechanisms"); } SS->sasl.n_mechs = num; } if (SS->sasl.sasl_ok && (SS->sasl.n_mechs > 0) && SS->sasl.mechlist && SS->sasl.mechlist[0]) { result = (NULL != strstr(SS->sasl.mechlist, "LOGIN")); } else result =0; if (SS->sasl.sasl_ok && SS->sasl.mechlist && SS->sasl.mechlist[0]) { type(SS, -250, NULL, "AUTH %s", SS->sasl.mechlist); if (result) { type(SS, -250, NULL, "AUTH=LOGIN"); /* RFC 2554, NetScape/ Sun Solstice/ ? */ type(SS, -250, NULL, "AUTH LOGIN"); /* RFC 2554, M$ Exchange ? */ } } else type(NULL,0,NULL, "AUTH -- no mechlist!"); } else #endif if (auth_ok) { if (auth_login_without_tls || SS->sslmode) { type(SS, -250, NULL, "AUTH=LOGIN"); /* RFC 2554, NetScape/ Sun Solstice/ ? */ type(SS, -250, NULL, "AUTH LOGIN"); /* RFC 2554, M$ Exchange ? */ } } }