Unify SMTP authentication with Cyrus SASL

This commit is contained in:
Mario Fetka
2026-07-17 21:36:13 +02:00
parent 17f6faf23d
commit 7e3a36e7d4
9 changed files with 241 additions and 112 deletions
+10
View File
@@ -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
+1
View File
@@ -13,6 +13,7 @@ target_link_libraries(bongosmtp
bongojson
bongomsgapi
bongomailauth
bongosasl
bongocollectoraccounts
)
+3 -2
View File
@@ -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") ||
+1 -1
View File
@@ -29,7 +29,7 @@ typedef struct {
int message_limit;
int etrn;
int starttls;
int auth;
const char *auth_mechanisms;
int smtputf8;
int expn;
} SMTPCapabilityOptions;
+175 -105
View File
@@ -42,6 +42,7 @@
#include <nmap.h>
#include <bongoagent.h>
#include <bongosasl.h>
#include <bongoutil.h>
#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);
+5 -1
View File
@@ -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"));
+1 -1
View File
@@ -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()
+25 -1
View File
@@ -25,6 +25,7 @@
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
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);
+20 -1
View File
@@ -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),