From: Stephan Bosch Date: Tue, 30 Sep 2025 01:35:23 +0000 (+0200) Subject: lib-smtp: test-smtp-payload - Add SASL authentication tests X-Git-Tag: 2.4.2~136 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f8d39a03fcabb76d8ade12db8a04f9eaaf90577d;p=thirdparty%2Fdovecot%2Fcore.git lib-smtp: test-smtp-payload - Add SASL authentication tests --- diff --git a/src/lib-smtp/Makefile.am b/src/lib-smtp/Makefile.am index a281f5bd0c..040cc17b96 100644 --- a/src/lib-smtp/Makefile.am +++ b/src/lib-smtp/Makefile.am @@ -5,6 +5,7 @@ AM_CPPFLAGS = \ -I$(top_srcdir)/src/lib-test \ -I$(top_srcdir)/src/lib-settings \ -I$(top_srcdir)/src/lib-master \ + -I$(top_srcdir)/src/lib-auth \ -I$(top_srcdir)/src/lib-sasl \ -I$(top_srcdir)/src/lib-auth-client \ -I$(top_srcdir)/src/lib-ssl-iostream \ @@ -114,6 +115,10 @@ test_libs = \ ../lib-settings/libsettings.la \ ../lib-dns/libdns.la \ ../lib-sasl/libsasl.la \ + ../lib-oauth2/liboauth2.la \ + ../lib-http/libhttp.la \ + ../lib-dcrypt/libdcrypt.la \ + ../lib-dict/libdict.la \ ../lib-auth/libauth.la \ ../lib-otp/libotp.la \ ../lib-json/libjson.la \ @@ -136,6 +141,10 @@ test_deps = \ ../lib-settings/libsettings.la \ ../lib-dns/libdns.la \ ../lib-sasl/libsasl.la \ + ../lib-oauth2/liboauth2.la \ + ../lib-http/libhttp.la \ + ../lib-dcrypt/libdcrypt.la \ + ../lib-dict/libdict.la \ ../lib-auth/libauth.la \ ../lib-json/libjson.la \ ../lib-var-expand/libvar_expand.la \ diff --git a/src/lib-smtp/test-smtp-payload.c b/src/lib-smtp/test-smtp-payload.c index 8bbfbd9e1a..16f1aa196c 100644 --- a/src/lib-smtp/test-smtp-payload.c +++ b/src/lib-smtp/test-smtp-payload.c @@ -4,6 +4,7 @@ #include "str.h" #include "llist.h" #include "array.h" +#include "base64.h" #include "path-util.h" #include "hostpid.h" #include "ioloop.h" @@ -16,6 +17,9 @@ #include "iostream-ssl-test.h" #include "iostream-openssl.h" #include "connection.h" +#include "password-scheme.h" +#include "dsasl-client.h" +#include "sasl-server.h" #include "test-common.h" #include "test-subprocess.h" #include "settings.h" @@ -51,6 +55,11 @@ static struct test_settings { unsigned int max_pending; bool unknown_size; enum test_ssl_mode ssl_mode; + + const char *sasl_mech; + const char *authid; + const char *authzid; + const char *password; } tset; static struct ip_addr bind_ip; @@ -163,6 +172,20 @@ struct client { struct client *prev, *next; struct smtp_server_connection *smtp_conn; + + struct { + struct smtp_server_cmd_ctx *cmd; + + struct sasl_server_req_ctx sasl_req; + const char *password_scheme; + sasl_server_passdb_callback_t *passdb_callback; + struct timeout *to_passdb; + + const char *authid; + const char *authzid; + const char *realm; + const char *password; + } auth; }; struct client_transaction { @@ -175,6 +198,8 @@ struct client_transaction { struct istream *payload, *file; }; +static struct sasl_server *sasl_server; +static struct sasl_server_instance *sasl_server_inst; static struct smtp_server *smtp_server; static struct io *io_listen; @@ -379,12 +404,280 @@ test_server_conn_cmd_data_continue(void *conn_ctx ATTR_UNUSED, return client_transaction_read_more(ctrans); } +/* authentication */ + +static int +test_server_conn_cmd_helo(void *conn_ctx ATTR_UNUSED, + struct smtp_server_cmd_ctx *cmd, + struct smtp_server_cmd_helo *data) +{ + struct smtp_server_reply *reply; + + reply = smtp_server_reply_create_ehlo(cmd->cmd); + if (data->helo.old_smtp) { + smtp_server_reply_submit(reply); + return 1; + } + + smtp_server_reply_ehlo_add_8bitmime(reply); + if (tset.sasl_mech != NULL) T_BEGIN { + struct sasl_server_mech_iter *mech_iter; + string_t *param = t_str_new(128); + + mech_iter = + sasl_server_instance_mech_iter_new(sasl_server_inst); + while (sasl_server_mech_iter_next(mech_iter)) { + if (str_len(param) > 0) + str_append_c(param, ' '); + str_append(param, mech_iter->name); + } + sasl_server_mech_iter_free(&mech_iter); + + if (str_len(param) > 0) { + smtp_server_reply_ehlo_add_param(reply, "AUTH", "%s", + str_c(param)); + } + } T_END; + smtp_server_reply_ehlo_add_binarymime(reply); + smtp_server_reply_ehlo_add_chunking(reply); + smtp_server_reply_ehlo_add_dsn(reply); + smtp_server_reply_ehlo_add_enhancedstatuscodes(reply); + smtp_server_reply_ehlo_add_starttls(reply); + smtp_server_reply_ehlo_add_pipelining(reply); + smtp_server_reply_ehlo_add_vrfy(reply); + smtp_server_reply_submit(reply); + return 1; +} + +static bool +test_server_sasl_set_authid(struct sasl_server_req_ctx *rctx, + enum sasl_server_authid_type authid_type, + const char *authid) +{ + struct client *client = + container_of(rctx, struct client, auth.sasl_req); + + test_assert(authid_type == SASL_SERVER_AUTHID_TYPE_USERNAME); + + client->auth.authid = p_strdup(client->pool, authid); + return TRUE; +} + +static bool +test_server_sasl_set_authzid(struct sasl_server_req_ctx *rctx, + const char *authzid) +{ + struct client *client = + container_of(rctx, struct client, auth.sasl_req); + + client->auth.authzid = p_strdup(client->pool, authzid); + return TRUE; +} + +static void +test_server_sasl_set_realm(struct sasl_server_req_ctx *rctx, + const char *realm) +{ + struct client *client = + container_of(rctx, struct client, auth.sasl_req); + + client->auth.realm = p_strdup(client->pool, realm); +} + +static void test_server_sasl_passdb_result(struct client *client) +{ + sasl_server_passdb_callback_t *callback = client->auth.passdb_callback; + struct sasl_passdb_result result; + + timeout_remove(&client->auth.to_passdb); + + i_zero(&result); + + if (tset.authid == NULL || + strcmp(client->auth.authid, tset.authid) != 0) { + result.status = SASL_PASSDB_RESULT_USER_UNKNOWN; + callback(&client->auth.sasl_req, &result); + return; + } + if (client->auth.authzid != NULL && + (tset.authzid == NULL || + strcmp(client->auth.authzid, tset.authzid) != 0)) { + result.status = SASL_PASSDB_RESULT_USER_UNKNOWN; + callback(&client->auth.sasl_req, &result); + return; + } + + if (client->auth.password != NULL) { + if (strcmp(tset.password, client->auth.password) == 0) { + result.status = SASL_PASSDB_RESULT_OK; + callback(&client->auth.sasl_req, &result); + } else { + result.status = SASL_PASSDB_RESULT_PASSWORD_MISMATCH; + callback(&client->auth.sasl_req, &result); + } + return; + } + + const struct password_generate_params params = { + .user = (client->auth.realm == NULL ? tset.authid : + t_strconcat(tset.authid, "@", client->auth.realm, NULL)), + }; + + if (!password_generate(tset.password, ¶ms, + client->auth.password_scheme, + &result.credentials.data, + &result.credentials.size)) { + i_zero(&result); + result.status = SASL_PASSDB_RESULT_INTERNAL_FAILURE; + callback(&client->auth.sasl_req, &result); + return; + } + + result.status = SASL_PASSDB_RESULT_OK; + callback(&client->auth.sasl_req, &result); +} + +static void +test_server_sasl_verify_plain(struct sasl_server_req_ctx *rctx, + const char *password, + sasl_server_passdb_callback_t *callback) +{ + struct client *client = + container_of(rctx, struct client, auth.sasl_req); + + i_assert(client->auth.to_passdb == NULL); + + /* Simulate async lookup */ + client->auth.passdb_callback = callback; + client->auth.password = p_strdup(client->pool, password); + client->auth.to_passdb = timeout_add_short(0, + test_server_sasl_passdb_result, client); +} + +static void +test_server_sasl_lookup_credentials(struct sasl_server_req_ctx *rctx, + const char *scheme, + sasl_server_passdb_callback_t *callback) +{ + struct client *client = + container_of(rctx, struct client, auth.sasl_req); + + i_assert(client->auth.to_passdb == NULL); + + /* Simulate async lookup */ + client->auth.passdb_callback = callback; + client->auth.password_scheme = p_strdup(client->pool, scheme); + client->auth.to_passdb = timeout_add_short(0, + test_server_sasl_passdb_result, client); +} + +static void +test_server_sasl_output(struct sasl_server_req_ctx *rctx, + const struct sasl_server_output *output) +{ + struct client *client = + container_of(rctx, struct client, auth.sasl_req); + struct smtp_server_cmd_ctx *cmd = client->auth.cmd; + + switch (output->status) { + case SASL_SERVER_OUTPUT_INTERNAL_FAILURE: + smtp_server_reply(cmd, 454, "4.7.0", "Internal error"); + return; + case SASL_SERVER_OUTPUT_PASSWORD_MISMATCH: + case SASL_SERVER_OUTPUT_FAILURE: + smtp_server_reply(cmd, 535, "5.7.8", "Authentication failed"); + return; + case SASL_SERVER_OUTPUT_SUCCESS: + if (output->data_size == 0) { + smtp_server_reply(cmd, 235, "2.7.0", "Logged in"); + return; + } + break; + case SASL_SERVER_OUTPUT_CONTINUE: + break; + } + + i_assert(output->data_size > 0); + + buffer_t *chal = t_base64_encode(0, 0, output->data, output->data_size); + smtp_server_cmd_auth_send_challenge(cmd, str_c(chal)); +} + +static int +test_server_conn_cmd_auth(void *conn_ctx, struct smtp_server_cmd_ctx *cmd, + struct smtp_server_cmd_auth *data) +{ + struct client *client = conn_ctx; + const struct sasl_server_mech *server_mech; + + server_mech = sasl_server_mech_find(sasl_server_inst, data->sasl_mech); + if (server_mech == NULL) { + smtp_server_reply(cmd, 504, "5.5.4", "Invalid mechanism"); + return -1; + } + + i_zero(&client->auth); + client->auth.cmd = cmd; + sasl_server_request_create(&client->auth.sasl_req, server_mech, "smtp", + NULL); + + const unsigned char *sasl_data; + size_t sasl_data_size; + + if (data->initial_response == NULL) { + sasl_data = NULL; + sasl_data_size = 0; + } else if (strcmp(data->initial_response, "=") == 0) { + sasl_data = uchar_empty_ptr; + sasl_data_size = 0; + } else { + size_t b64_size = strlen(data->initial_response); + buffer_t *resp = t_buffer_create(MAX_BASE64_DECODED_SIZE(b64_size)); + if (base64_decode(data->initial_response, b64_size, resp) < 0) { + smtp_server_reply(cmd, 501, "5.5.2", + "Invalid Base64 encoding"); + return -1; + } + + sasl_data = resp->data; + sasl_data_size = resp->used; + } + sasl_server_request_initial(&client->auth.sasl_req, + sasl_data, sasl_data_size); + return 0; +} + +static int +test_server_conn_cmd_auth_continue(void *conn_ctx, + struct smtp_server_cmd_ctx *cmd, + const char *response) +{ + struct client *client = conn_ctx; + + size_t b64_size = strlen(response); + buffer_t *resp = t_buffer_create(MAX_BASE64_DECODED_SIZE(b64_size)); + if (base64_decode(response, b64_size, resp) < 0) { + smtp_server_reply(cmd, 501, "5.5.2", + "Invalid Base64 encoding"); + sasl_server_request_destroy(&client->auth.sasl_req); + return -1; + } + + sasl_server_request_input(&client->auth.sasl_req, + resp->data, resp->used); + return 0; +} + /* client connection */ static void test_server_connection_free(void *context); static const struct smtp_server_callbacks server_callbacks = { + .conn_cmd_helo = test_server_conn_cmd_helo, + .conn_cmd_auth = test_server_conn_cmd_auth, + .conn_cmd_auth_continue = test_server_conn_cmd_auth_continue, + .conn_cmd_rcpt = test_server_conn_cmd_rcpt, .conn_cmd_data_begin = test_server_conn_cmd_data_begin, .conn_cmd_data_continue = test_server_conn_cmd_data_continue, @@ -425,6 +718,8 @@ static void client_deinit(struct client **_client) smtp_server_connection_terminate(&client->smtp_conn, NULL, "deinit"); } + timeout_remove(&client->auth.to_passdb); + sasl_server_request_destroy(&client->auth.sasl_req); pool_unref(&client->pool); } @@ -456,11 +751,34 @@ static void client_accept(void *context ATTR_UNUSED) /* */ +static const struct sasl_server_request_funcs server_sasl_funcs = { + .request_set_authid = test_server_sasl_set_authid, + .request_set_authzid = test_server_sasl_set_authzid, + .request_set_realm = test_server_sasl_set_realm, + + .request_verify_plain = test_server_sasl_verify_plain, + .request_lookup_credentials = test_server_sasl_lookup_credentials, + + .request_output = test_server_sasl_output, +}; + static void test_server_init(const struct smtp_server_settings *server_set) { + const struct sasl_server_settings sasl_set = {}; + /* open server socket */ io_listen = io_add(fd_listen, IO_READ, client_accept, NULL); + /* init SASL server */ + sasl_server = sasl_server_init(server_set->event_parent, + &server_sasl_funcs); + sasl_server_inst = sasl_server_instance_create(sasl_server, &sasl_set); + + sasl_server_mech_register_plain(sasl_server_inst); + sasl_server_mech_register_login(sasl_server_inst); + sasl_server_mech_register_scram_sha256(sasl_server_inst); + + /* init SMTP server */ smtp_server = smtp_server_init(server_set); } @@ -471,6 +789,8 @@ static void test_server_deinit(void) /* deinitialize */ smtp_server_deinit(&smtp_server); + sasl_server_instance_unref(&sasl_server_inst); + sasl_server_deinit(&sasl_server); } /* @@ -525,10 +845,18 @@ static struct test_client_connection *test_client_connection_get(void) } if (test_conns[i].conn == NULL) { + struct smtp_client_settings set; + + i_zero(&set); + if (tset.sasl_mech != NULL) { + i_assert(tset.authid != NULL); + set.username = tset.authid; + set.password = tset.password; + } + test_conns[i].conn = smtp_client_connection_create( smtp_client, client_protocol, - net_ip2addr(&bind_ip), bind_port, - ssl_mode, NULL); + net_ip2addr(&bind_ip), bind_port, ssl_mode, &set); } return &test_conns[i]; } @@ -1003,7 +1331,7 @@ test_run_scenarios( smtp_server_set.hostname = "localhost"; smtp_server_set.max_client_idle_time_msecs = CLIENT_PROGRESS_TIMEOUT_MSECS; smtp_server_set.max_pipelined_commands = 1; - smtp_server_set.auth_optional = TRUE; + smtp_server_set.auth_optional = (tset.sasl_mech == NULL); smtp_server_set.ssl = &ssl_server_set; smtp_server_set.debug = debug; @@ -1013,6 +1341,7 @@ test_run_scenarios( smtp_client_set.temp_path_prefix = "/tmp"; smtp_client_set.command_timeout_msecs = CLIENT_PROGRESS_TIMEOUT_MSECS; smtp_client_set.connect_timeout_msecs = CLIENT_PROGRESS_TIMEOUT_MSECS; + smtp_client_set.sasl_mechanisms = tset.sasl_mech; smtp_client_set.ssl = &ssl_client_set; smtp_client_set.debug = debug; @@ -1124,11 +1453,46 @@ static void test_lmtp_chunking(void) test_end(); } + +static void test_smtp_authentication(void) +{ + i_zero(&tset); + test_begin("smtp payload - auth - PLAIN"); + tset.sasl_mech = "PLAIN"; + tset.authid = "user"; + tset.password = "password"; + test_run_scenarios(SMTP_PROTOCOL_SMTP, + SMTP_CAPABILITY_DSN | SMTP_CAPABILITY_AUTH, + test_client); + test_end(); + + i_zero(&tset); + test_begin("smtp payload - auth - LOGIN"); + tset.sasl_mech = "LOGIN"; + tset.authid = "user"; + tset.password = "password"; + test_run_scenarios(SMTP_PROTOCOL_SMTP, + SMTP_CAPABILITY_DSN | SMTP_CAPABILITY_AUTH, + test_client); + test_end(); + + i_zero(&tset); + test_begin("smtp payload - auth - SCRAM-SHA-256"); + tset.sasl_mech = "SCRAM-SHA-256"; + tset.authid = "user"; + tset.password = "password"; + test_run_scenarios(SMTP_PROTOCOL_SMTP, + SMTP_CAPABILITY_DSN | SMTP_CAPABILITY_AUTH, + test_client); + test_end(); +} + static void (*const test_functions[])(void) = { test_smtp_normal, test_smtp_chunking, test_lmtp_normal, test_lmtp_chunking, + test_smtp_authentication, NULL }; @@ -1139,10 +1503,14 @@ static void (*const test_functions[])(void) = { static void main_init(void) { ssl_iostream_openssl_init(); + password_schemes_init(); + dsasl_clients_init(); } static void main_deinit(void) { + dsasl_clients_deinit(); + password_schemes_deinit(); ssl_iostream_context_cache_free(); ssl_iostream_openssl_deinit(); }