diff --git a/docs/authentication.md b/docs/authentication.md index 571f729..a5aa072 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -30,6 +30,16 @@ source IPv4 or IPv6 network. Requiring app passwords remains a separate, explicit per-user policy; enabling TOTP does not silently change mail-client authentication. +SMTP submission, IMAP, POP3 and ManageSieve use one Cyrus SASL server layer. +Its Bongo callback applies the same account lookup, service-specific app +password policy, source-network restriction and authentication audit trail to +every protocol. Password-bearing mechanisms are advertised only after TLS and +the advertised list is generated from mechanisms that are both installed and +enabled by Bongo. Clients cannot select an installed Cyrus mechanism that +Bongo did not advertise. Bongo 0.7 enables `PLAIN` and `LOGIN` for TLS-protected +compatibility clients; adding a mechanism requires implementing its credential +callback and policy first, not merely installing another Cyrus plugin. + ## LDAP master authentication LDAP can be enabled as the primary source for normal account passwords. It is diff --git a/src/agents/smtp/CMakeLists.txt b/src/agents/smtp/CMakeLists.txt index c8bf02e..ceacd81 100644 --- a/src/agents/smtp/CMakeLists.txt +++ b/src/agents/smtp/CMakeLists.txt @@ -13,6 +13,7 @@ target_link_libraries(bongosmtp bongojson bongomsgapi bongomailauth + bongosasl bongocollectoraccounts ) diff --git a/src/agents/smtp/capabilities.c b/src/agents/smtp/capabilities.c index 274debb..b73a368 100644 --- a/src/agents/smtp/capabilities.c +++ b/src/agents/smtp/capabilities.c @@ -67,8 +67,9 @@ SMTPFormatEhlo(char *buffer, size_t buffer_size, !Append(buffer, buffer_size, &length, "250-ETRN\r\n")) || (options->starttls && !Append(buffer, buffer_size, &length, "250-STARTTLS\r\n")) || - (options->auth && - !Append(buffer, buffer_size, &length, "250-AUTH PLAIN LOGIN\r\n")) || + (options->auth_mechanisms && *options->auth_mechanisms && + !Append(buffer, buffer_size, &length, "250-AUTH %s\r\n", + options->auth_mechanisms)) || (options->smtputf8 && !Append(buffer, buffer_size, &length, "250-SMTPUTF8\r\n")) || !Append(buffer, buffer_size, &length, "250-HELP\r\n") || diff --git a/src/agents/smtp/capabilities.h b/src/agents/smtp/capabilities.h index 796a10a..3c7de9f 100644 --- a/src/agents/smtp/capabilities.h +++ b/src/agents/smtp/capabilities.h @@ -29,7 +29,7 @@ typedef struct { int message_limit; int etrn; int starttls; - int auth; + const char *auth_mechanisms; int smtputf8; int expn; } SMTPCapabilityOptions; diff --git a/src/agents/smtp/smtpd.c b/src/agents/smtp/smtpd.c index 73e3bb6..03c7b91 100644 --- a/src/agents/smtp/smtpd.c +++ b/src/agents/smtp/smtpd.c @@ -42,6 +42,7 @@ #include #include +#include #include #include "mailauth.h" #include "smtpd.h" @@ -719,20 +720,35 @@ IsTransportRecipient(const char *address) return FALSE; } -static BOOL -AuthenticateUser(ConnectionStruct *client, const char *user, const char *password) +static int +SMTPVerifyPassword(void *context, const char *user, const char *password, + size_t passwordLength) { + ConnectionStruct *client = context; char source[INET_ADDRSTRLEN]; + char *copy; + int valid; - if (user == NULL || password == NULL || *user == '\0' || + if (client == NULL || user == NULL || password == NULL || *user == '\0' || + passwordLength > 4096U || !inet_ntop(AF_INET, &client->client.conn->socketAddress.sin_addr, - source, sizeof(source)) || - MsgAuthVerifyServicePassword(user, password, MSG_AUTH_SERVICE_SMTP, - source) != 0 || MsgAuthFindUser(user) != 0) { - Log(LOG_NOTICE, "Bongo authentication failed: service=smtp source=%s", - LOGIP(client->client.conn->socketAddress)); - return FALSE; - } + source, sizeof(source))) return 0; + copy = malloc(passwordLength + 1U); + if (copy == NULL) return 0; + memcpy(copy, password, passwordLength); + copy[passwordLength] = '\0'; + valid = MsgAuthVerifyServicePassword(user, copy, MSG_AUTH_SERVICE_SMTP, + source) == 0 && + MsgAuthFindUser(user) == 0; + memset(copy, 0, passwordLength + 1U); + free(copy); + return valid; +} + +static BOOL +SMTPRecordAuthenticatedUser(ConnectionStruct *client, const char *user) +{ + if (client == NULL || user == NULL || *user == '\0') return FALSE; Log(LOG_NOTICE, "Successful login for user %s at host %s", user, LOGIP(client->client.conn->socketAddress)); @@ -741,6 +757,131 @@ AuthenticateUser(ConnectionStruct *client, const char *user, const char *passwor return client->AuthFrom != NULL; } +enum { + SMTP_SASL_CONNECTION_CLOSED = -1, + SMTP_SASL_FAILED = 0, + SMTP_SASL_COMPLETE = 1, + SMTP_SASL_CANCELLED = 2 +}; + +static int +SMTPAuthenticateSasl(ConnectionStruct *client, char *arguments) +{ + BongoSaslSession *session = NULL; + const void *output = NULL; + const char *username; + char mechanism[64]; + char decoded[BUFSIZE + 1]; + char encoded[(BUFSIZE + 1) * 2]; + char *initial; + size_t mechanismLength = 0U; + size_t decodedLength = 0U; + size_t encodedLength = 0U; + size_t outputLength = 0U; + int saslResult; + int result = SMTP_SASL_FAILED; + + if (client == NULL || arguments == NULL || + !isspace((unsigned char)*arguments)) return SMTP_SASL_FAILED; + while (isspace((unsigned char)*arguments)) arguments++; + while (arguments[mechanismLength] != '\0' && + !isspace((unsigned char)arguments[mechanismLength])) { + mechanismLength++; + } + if (mechanismLength == 0U || mechanismLength >= sizeof(mechanism)) { + return SMTP_SASL_FAILED; + } + memcpy(mechanism, arguments, mechanismLength); + mechanism[mechanismLength] = '\0'; + for (size_t index = 0U; index < mechanismLength; index++) { + mechanism[index] = (char)toupper((unsigned char)mechanism[index]); + } + initial = arguments + mechanismLength; + while (isspace((unsigned char)*initial)) initial++; + if (!strcmp(initial, "*")) { + ConnWrite(client->client.conn, + "501 5.7.0 Authentication canceled\r\n", + sizeof("501 5.7.0 Authentication canceled\r\n") - 1U); + return SMTP_SASL_CANCELLED; + } + if (!BongoSaslSessionCreate(&session, "smtp", BongoGlobals.hostname, + NULL, NULL, 1, SMTPVerifyPassword, client)) { + return SMTP_SASL_FAILED; + } + if (*initial != '\0' && strcmp(initial, "=") && + !BongoSaslDecode64(initial, strlen(initial), decoded, sizeof(decoded), + &decodedLength)) goto finish; + + saslResult = BongoSaslStart(session, mechanism, + *initial != '\0' ? decoded : NULL, + decodedLength, &output, &outputLength); + while (saslResult == BONGO_SASL_CONTINUE) { + encodedLength = 0U; + if (outputLength != 0U && + !BongoSaslEncode64(output, outputLength, encoded, sizeof(encoded), + &encodedLength)) goto finish; + if (ConnWriteF(client->client.conn, "334 %.*s\r\n", (int)encodedLength, + outputLength != 0U ? encoded : "") == -1 || + ConnFlush(client->client.conn) == -1) { + result = SMTP_SASL_CONNECTION_CLOSED; + goto finish; + } + if (ConnReadToAllocatedBuffer(client->client.conn, + &client->client.buffer, + &client->client.buflen) <= 0) { + result = SMTP_SASL_CONNECTION_CLOSED; + goto finish; + } + if (!strcmp(client->client.buffer, "*")) { + ConnWrite(client->client.conn, + "501 5.7.0 Authentication canceled\r\n", + sizeof("501 5.7.0 Authentication canceled\r\n") - 1U); + result = SMTP_SASL_CANCELLED; + goto finish; + } + if (!BongoSaslDecode64(client->client.buffer, + strlen(client->client.buffer), decoded, + sizeof(decoded), &decodedLength)) goto finish; + saslResult = BongoSaslStep(session, decoded, decodedLength, + &output, &outputLength); + } + username = BongoSaslUsername(session); + if (saslResult == BONGO_SASL_COMPLETE && username != NULL && + SMTPRecordAuthenticatedUser(client, username)) { + result = SMTP_SASL_COMPLETE; + } + +finish: + memset(decoded, 0, sizeof(decoded)); + memset(encoded, 0, sizeof(encoded)); + if (client->client.buffer != NULL && client->client.buflen != 0U) { + memset(client->client.buffer, 0, client->client.buflen); + } + BongoSaslSessionDestroy(&session); + return result; +} + +static BOOL +SMTPGetSaslMechanisms(ConnectionStruct *client, char *buffer, size_t bufferSize) +{ + BongoSaslSession *session = NULL; + const char *mechanisms = NULL; + BOOL available = FALSE; + + if (client == NULL || buffer == NULL || bufferSize == 0U || + !BongoSaslSessionCreate(&session, "smtp", BongoGlobals.hostname, + NULL, NULL, 1, SMTPVerifyPassword, client)) { + return FALSE; + } + if (BongoSaslMechanisms(session, &mechanisms) && mechanisms != NULL && + *mechanisms != '\0' && strlen(mechanisms) < bufferSize) { + strcpy(buffer, mechanisms); + available = TRUE; + } + BongoSaslSessionDestroy(&session); + return available; +} + static BOOL SubmissionSenderAllowed(const char *user, const char *sender) { @@ -766,42 +907,6 @@ SubmissionSenderAllowed(const char *user, const char *sender) return found == 1; } -static BOOL -ParsePlainCredentials(char *encoded, const char **user, const char **password) -{ - size_t encodedLength; - size_t decodedLimit; - char *decoded; - char *authcid; - char *separator; - - if (encoded == NULL || user == NULL || password == NULL) { - return FALSE; - } - encodedLength = strlen(encoded); - if (encodedLength == 0 || encodedLength > BUFSIZE) { - return FALSE; - } - decodedLimit = ((encodedLength + 3U) / 4U) * 3U; - decoded = DecodeBase64(encoded); - if (decoded == NULL) { - return FALSE; - } - - separator = memchr(decoded, '\0', decodedLimit); - if (separator == NULL) { - return FALSE; - } - authcid = separator + 1; - separator = memchr(authcid, '\0', decodedLimit - (size_t)(authcid - decoded)); - if (separator == NULL || authcid == separator) { - return FALSE; - } - *user = authcid; - *password = separator + 1; - return (size_t)(*password - decoded) < decodedLimit; -} - typedef struct { char To[MAXEMAILNAMESIZE+1]; @@ -1133,9 +1238,7 @@ HandleConnection (void *param) } case 'A':{ /* AUTH */ - const char *user = NULL; - const char *password = NULL; - BOOL authenticated = FALSE; + int authResult; if (AllowAuth == FALSE || !Client->client.conn->ssl.enable) { ConnWrite (Client->client.conn, MSG500UNKNOWN, MSG500UNKNOWN_LEN); @@ -1147,67 +1250,25 @@ HandleConnection (void *param) ConnWrite (Client->client.conn, MSG503BADORDER, MSG503BADORDER_LEN); break; } - - if (XplStrNCaseCmp(Client->Command + 5, "PLAIN", 5) == 0) { - char *encoded = Client->Command + 10; - - while (isspace((unsigned char)*encoded)) { - encoded++; - } - if (*encoded == '\0') { - ConnWrite(Client->client.conn, "334 \r\n", 6); - ConnFlush(Client->client.conn); - if (ConnReadToAllocatedBuffer(Client->client.conn, - &Client->client.buffer, &Client->client.buflen) <= 0) { - return EndClientConnection(Client); - } - encoded = Client->client.buffer; - } - if (strcmp(encoded, "*") == 0) { - ConnWrite(Client->client.conn, - "501 5.7.0 Authentication canceled\r\n", 35); - break; - } - if (ParsePlainCredentials(encoded, &user, &password)) { - authenticated = AuthenticateUser(Client, user, password); - } - } else if (XplStrNCaseCmp(Client->Command + 5, "LOGIN", 5) == 0) { - if (!isspace((unsigned char)Client->Command[10])) { - ConnWrite (Client->client.conn, "334 VXNlcm5hbWU6\r\n", 18); - ConnFlush (Client->client.conn); - if (ConnReadToAllocatedBuffer(Client->client.conn, - &Client->client.buffer, &Client->client.buflen) <= 0) { - return EndClientConnection(Client); - } - snprintf(Reply, sizeof(Reply), "%s", Client->client.buffer); - } else { - snprintf(Reply, sizeof(Reply), "%s", Client->Command + 11); - } - if (strcmp(Reply, "*") != 0) { - user = DecodeBase64(Reply); - ConnWrite(Client->client.conn, "334 UGFzc3dvcmQ6\r\n", 18); - ConnFlush(Client->client.conn); - if (ConnReadToAllocatedBuffer(Client->client.conn, - &Client->client.buffer, &Client->client.buflen) <= 0) { - return EndClientConnection(Client); - } - if (strcmp(Client->client.buffer, "*") != 0) { - password = DecodeBase64(Client->client.buffer); - authenticated = AuthenticateUser(Client, user, password); - } - } - } else { - ConnWrite(Client->client.conn, MSG504BADAUTH, MSG504BADAUTH_LEN); + if (Client->State != STATE_HELO || !Client->IsEHLO) { + ConnWrite(Client->client.conn, MSG503BADORDER, + MSG503BADORDER_LEN); break; } - - if (authenticated) { + authResult = SMTPAuthenticateSasl(Client, Client->Command + 4); + if (authResult == SMTP_SASL_CONNECTION_CLOSED) { + return EndClientConnection(Client); + } + if (authResult == SMTP_SASL_COMPLETE) { Client->State = STATE_AUTH; IsAuthed = TRUE; IsTrusted = TRUE; ConnWrite(Client->client.conn, "235 2.7.0 Authentication successful\r\n", 37); - } else { + } else if (authResult == SMTP_SASL_FAILED) { + Log(LOG_NOTICE, + "Bongo authentication failed: service=smtp source=%s", + LOGIP(Client->client.conn->socketAddress)); ConnWrite(Client->client.conn, "535 5.7.8 Authentication credentials invalid\r\n", 46); } @@ -2330,6 +2391,7 @@ HandleConnection (void *param) case 'H':{ /* EHLO */ if (Client->State == STATE_FRESH) { SMTPCapabilityOptions capabilities; + char mechanisms[256]; size_t answerLength; XplRWReadLockAcquire (&ConfigLock); @@ -2340,8 +2402,11 @@ HandleConnection (void *param) (SMTP.allow_client_ssl || Client->InternalRelay || Client->Submission) && (Client->InternalRelay ? InternalSSLContext != NULL : SSLContext != NULL) && !Client->client.conn->ssl.enable; - capabilities.auth = AllowAuth == TRUE && - Client->client.conn->ssl.enable; + capabilities.auth_mechanisms = + AllowAuth == TRUE && Client->client.conn->ssl.enable && + SMTPGetSaslMechanisms(Client, mechanisms, + sizeof(mechanisms)) + ? mechanisms : NULL; capabilities.smtputf8 = SMTP.smtp_utf8_enabled; capabilities.expn = SMTP.allow_expn; answerLength = SMTPFormatEhlo(Answer, sizeof(Answer), @@ -5026,6 +5091,7 @@ SMTPServer (void *ignored) LogShutdown (); XplRWLockDestroy (&ConfigLock); + BongoSaslShutdown(); MsgShutdown (); ConnShutdown (); @@ -5310,6 +5376,10 @@ int XplServiceMain (int argc, char *argv[]) MsgInit(); MsgAuthInit(); + if (!BongoSaslInitialize()) { + Log(LOG_ERROR, "Unable to initialize Cyrus SASL"); + return -1; + } // FIXME: Connio socket timeout needs to be a run-time tunable. bug #9924 // ConnStartup (SMTP.socket_timeout, TRUE); diff --git a/src/agents/smtp/tests/capabilities-test.c b/src/agents/smtp/tests/capabilities-test.c index 7f49a2f..044ba57 100644 --- a/src/agents/smtp/tests/capabilities-test.c +++ b/src/agents/smtp/tests/capabilities-test.c @@ -46,7 +46,7 @@ main(void) options.etrn = 1; options.starttls = 1; - options.auth = 1; + options.auth_mechanisms = "PLAIN LOGIN"; options.smtputf8 = 1; options.expn = 1; assert(SMTPFormatEhlo(buffer, sizeof(buffer), &options) > 0); @@ -56,6 +56,10 @@ main(void) assert(strstr(buffer, "250-SMTPUTF8\r\n")); assert(strstr(buffer, "250-EXPN\r\n")); + options.auth_mechanisms = "SCRAM-SHA-256"; + assert(SMTPFormatEhlo(buffer, sizeof(buffer), &options) > 0); + assert(strstr(buffer, "250-AUTH SCRAM-SHA-256\r\n")); + options.message_limit = 0; assert(SMTPFormatEhlo(buffer, sizeof(buffer), &options) > 0); assert(strstr(buffer, "250 SIZE\r\n")); diff --git a/src/libs/sasl/CMakeLists.txt b/src/libs/sasl/CMakeLists.txt index f040eb6..2742a35 100644 --- a/src/libs/sasl/CMakeLists.txt +++ b/src/libs/sasl/CMakeLists.txt @@ -5,5 +5,5 @@ install(TARGETS bongosasl DESTINATION ${LIB_INSTALL_DIR}) if(BUILD_TESTING) add_executable(bongo-sasl-test tests/sasl-test.c) target_link_libraries(bongo-sasl-test PRIVATE bongosasl) - add_test(NAME sasl-plain COMMAND bongo-sasl-test) + add_test(NAME sasl-password-mechanisms COMMAND bongo-sasl-test) endif() diff --git a/src/libs/sasl/sasl.c b/src/libs/sasl/sasl.c index e0e1770..b846501 100644 --- a/src/libs/sasl/sasl.c +++ b/src/libs/sasl/sasl.c @@ -25,6 +25,7 @@ #include #include #include +#include struct BongoSaslSession { sasl_conn_t *connection; @@ -135,6 +136,28 @@ exchange_result(int result, const char *sasl_output, unsigned sasl_length, return BONGO_SASL_FAIL; } +static int +mechanism_allowed(BongoSaslSession *session, const char *mechanism) +{ + const char *allowed; + const char *cursor; + size_t mechanism_length; + + if (!session || !mechanism || !*mechanism || + !BongoSaslMechanisms(session, &allowed)) return 0; + mechanism_length = strlen(mechanism); + for (cursor = allowed; *cursor;) { + const char *end; + while (*cursor == ' ') cursor++; + end = strchr(cursor, ' '); + if (!end) end = cursor + strlen(cursor); + if ((size_t)(end - cursor) == mechanism_length && + !strncasecmp(cursor, mechanism, mechanism_length)) return 1; + cursor = end; + } + return 0; +} + int BongoSaslStart(BongoSaslSession *session, const char *mechanism, const void *input, size_t input_length, @@ -143,7 +166,8 @@ BongoSaslStart(BongoSaslSession *session, const char *mechanism, const char *sasl_output = NULL; unsigned sasl_length = 0; int result; - if (!session || !mechanism || input_length > (size_t)UINT_MAX) return BONGO_SASL_FAIL; + if (!session || !mechanism || input_length > (size_t)UINT_MAX || + !mechanism_allowed(session, mechanism)) return BONGO_SASL_FAIL; result = sasl_server_start(session->connection, mechanism, input, (unsigned)input_length, &sasl_output, &sasl_length); return exchange_result(result, sasl_output, sasl_length, output, output_length); diff --git a/src/libs/sasl/tests/sasl-test.c b/src/libs/sasl/tests/sasl-test.c index 1ef8b40..cefd26a 100644 --- a/src/libs/sasl/tests/sasl-test.c +++ b/src/libs/sasl/tests/sasl-test.c @@ -46,11 +46,30 @@ int main(void) if (!BongoSaslSessionCreate(&session, "sieve", "mail.example.test", "127.0.0.1;4190", "127.0.0.1;12345", 1, verify, NULL)) goto finish; - if (!BongoSaslMechanisms(session, &mechanisms) || !strstr(mechanisms, "PLAIN")) goto finish; + if (!BongoSaslMechanisms(session, &mechanisms) || + !strstr(mechanisms, "PLAIN") || !strstr(mechanisms, "LOGIN")) goto finish; if (BongoSaslStart(session, "PLAIN", plain, sizeof(plain) - 1, &output, &output_length) != BONGO_SASL_COMPLETE) goto finish; if (!BongoSaslUsername(session) || strcmp(BongoSaslUsername(session), "alice@example.test")) goto finish; + BongoSaslSessionDestroy(&session); + if (!BongoSaslSessionCreate(&session, "smtp", "mail.example.test", + "127.0.0.1;587", "127.0.0.1;12345", 1, + verify, NULL)) goto finish; + if (BongoSaslStart(session, "SCRAM-SHA-256", NULL, 0, &output, + &output_length) != BONGO_SASL_FAIL) goto finish; + BongoSaslSessionDestroy(&session); + if (!BongoSaslSessionCreate(&session, "smtp", "mail.example.test", + "127.0.0.1;587", "127.0.0.1;12345", 1, + verify, NULL)) goto finish; + if (BongoSaslStart(session, "login", NULL, 0, &output, + &output_length) != BONGO_SASL_CONTINUE || + BongoSaslStep(session, "alice@example.test", 18, &output, + &output_length) != BONGO_SASL_CONTINUE || + BongoSaslStep(session, "secret", 6, &output, + &output_length) != BONGO_SASL_COMPLETE || + !BongoSaslUsername(session) || + strcmp(BongoSaslUsername(session), "alice@example.test")) goto finish; if (!BongoSaslEncode64(plain, sizeof(plain) - 1, encoded, sizeof(encoded), &encoded_length) || !BongoSaslDecode64(encoded, encoded_length, decoded, sizeof(decoded),