From: Stephan Bosch Date: Sun, 5 Oct 2025 16:58:37 +0000 (+0200) Subject: lib-sasl: Add GSSAPI client support X-Git-Tag: 2.4.2~127 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=576bb6b5cf1c0b381a21e02f0330cb1ec3070267;p=thirdparty%2Fdovecot%2Fcore.git lib-sasl: Add GSSAPI client support --- diff --git a/src/lib-sasl/Makefile.am b/src/lib-sasl/Makefile.am index 9cf297712a..70d9765d38 100644 --- a/src/lib-sasl/Makefile.am +++ b/src/lib-sasl/Makefile.am @@ -53,6 +53,7 @@ libsasl_la_DEPENDENCIES = \ if HAVE_GSSAPI libsasl_gssapi_la_SOURCES = \ + dsasl-client-mech-gssapi.c \ sasl-server-mech-gssapi.c libsasl_gssapi_la_LIBADD = $(KRB5_LIBS) libsasl_gssapi_la_CPPFLAGS = $(AM_CPPFLAGS) $(KRB5_CFLAGS) @@ -77,7 +78,8 @@ pkginc_libdir=$(pkgincludedir) pkginc_lib_HEADERS = $(headers) noinst_HEADERS =\ - dsasl-client-mech-ntlm-dummy.h + dsasl-client-mech-ntlm-dummy.h \ + gssapi-dummy.h test_programs = \ test-sasl-oauth2 \ @@ -117,6 +119,17 @@ test_sasl_authentication_SOURCES = \ test-sasl-authentication.c test_sasl_authentication_LDADD = $(test_libs) test_sasl_authentication_DEPENDENCIES = $(test_deps) +if HAVE_GSSAPI +test_sasl_authentication_SOURCES += \ + gssapi-dummy.c \ + krb5-dummy.c \ + dsasl-client-mech-gssapi.c \ + sasl-server-mech-gssapi.c +test_sasl_authentication_LDADD += \ + ../lib-auth/libauth-gssapi.la +test_sasl_authentication_DEPENDENCIES += \ + ../lib-auth/libauth-gssapi.la +endif ntlm_dummy_SOURCES = ntlm_dummy.c ntlm_dummy_LDADD = ../lib/liblib.la @@ -130,6 +143,17 @@ fuzz_sasl_authentication_SOURCES = \ fuzz-sasl-authentication.c fuzz_sasl_authentication_LDADD = $(test_libs) fuzz_sasl_authentication_DEPENDENCIES = $(test_deps) +if HAVE_GSSAPI +fuzz_sasl_authentication_SOURCES += \ + gssapi-dummy.c \ + krb5-dummy.c \ + dsasl-client-mech-gssapi.c \ + sasl-server-mech-gssapi.c +fuzz_sasl_authentication_LDADD += \ + ../lib-auth/libauth-gssapi.la +fuzz_sasl_authentication_DEPENDENCIES += \ + ../lib-auth/libauth-gssapi.la +endif check-local: for bin in $(test_programs); do \ diff --git a/src/lib-sasl/dsasl-client-mech-gssapi.c b/src/lib-sasl/dsasl-client-mech-gssapi.c new file mode 100644 index 0000000000..6e0f5e0259 --- /dev/null +++ b/src/lib-sasl/dsasl-client-mech-gssapi.c @@ -0,0 +1,374 @@ +/* Copyright (c) 2025 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "str.h" +#include "str-sanitize.h" +#include "buffer.h" +#include "auth-gssapi.h" + +#include "dsasl-client-private.h" + +/* Non-zero flags defined in RFC 2222 */ +enum sasl_gssapi_qop { + SASL_GSSAPI_QOP_UNSPECIFIED = 0x00, + SASL_GSSAPI_QOP_AUTH_ONLY = 0x01, + SASL_GSSAPI_QOP_AUTH_INT = 0x02, + SASL_GSSAPI_QOP_AUTH_CONF = 0x04 +}; + +enum gssapi_gs1_client_state { + GSSAPI_GS1_CLIENT_STATE_INIT = 0, + GSSAPI_GS1_CLIENT_STATE_SEC_CONTEXT, + GSSAPI_GS1_CLIENT_STATE_UNWRAP, + GSSAPI_GS1_CLIENT_STATE_WRAP, + GSSAPI_GS1_CLIENT_STATE_END, +}; + +struct gssapi_sasl_client { + struct dsasl_client client; + + union { + enum gssapi_gs1_client_state gs1; + } state; + + gss_name_t gss_principal; + gss_ctx_id_t gss_ctx; + + buffer_t *out_buf; +}; + +static void +mech_gssapi_error_append(string_t *msg, unsigned int *entries, + OM_uint32 status_value, int status_type) +{ + OM_uint32 major_status, minor_status; + OM_uint32 message_context = 0; + gss_buffer_desc status_string; + + do { + major_status = gss_display_status(&minor_status, status_value, + status_type, GSS_C_NO_OID, + &message_context, + &status_string); + if (major_status == GSS_S_COMPLETE && + status_string.length > 0) { + if ((*entries)++ > 0) + str_append(msg, "; "); + str_append(msg, str_sanitize(status_string.value, + status_string.length)); + } + (void)gss_release_buffer(&minor_status, &status_string); + } while (GSS_ERROR(major_status) == 0 && message_context != 0); +} + +static const char * +mech_gssapi_error(const char *description, OM_uint32 major_status, + OM_uint32 minor_status) +{ + string_t *msg = t_str_new(128); + unsigned int entries = 0; + + str_append(msg, "While "); + str_append(msg, description); + str_append(msg, ": "); + + if (major_status != GSS_S_FAILURE) { + mech_gssapi_error_append(msg, &entries, major_status, + GSS_C_GSS_CODE); + } + mech_gssapi_error_append(msg, &entries, minor_status, GSS_C_MECH_CODE); + + return str_c(msg); +} + +static enum dsasl_client_result +mech_gssapi_gs1_init(struct gssapi_sasl_client *gclient, gss_buffer_t in_buf, + const char **error_r) +{ + struct dsasl_client *client = &gclient->client; + OM_uint32 major_status, minor_status; + string_t *principal_name; + gss_buffer_desc namebuf; + + gclient->gss_ctx = GSS_C_NO_CONTEXT; + gclient->gss_principal = GSS_C_NO_NAME; + + if (in_buf->length > 0) { + *error_r = "Unexpected initial server challenge"; + return DSASL_CLIENT_RESULT_ERR_PROTOCOL; + } + + principal_name = t_str_new(128); + str_append(principal_name, client->set.protocol); + str_append_c(principal_name, '@'); + str_append(principal_name, client->set.host); + + namebuf.length = str_len(principal_name); + namebuf.value = str_c_modifiable(principal_name); + + major_status = gss_import_name(&minor_status, &namebuf, + GSS_C_NT_HOSTBASED_SERVICE, + &gclient->gss_principal); + if (GSS_ERROR(major_status) != 0) { + *error_r = mech_gssapi_error("importing principal name", + major_status, minor_status); + return DSASL_CLIENT_RESULT_ERR_PROTOCOL; + } + + e_debug(client->event, "Successfully imported principal name"); + + gclient->out_buf = buffer_create_dynamic(default_pool, 1024); + gclient->state.gs1 = GSSAPI_GS1_CLIENT_STATE_SEC_CONTEXT; + return DSASL_CLIENT_RESULT_OK; +} + +static int +mech_gssapi_sec_context(struct gssapi_sasl_client *gclient, + gss_buffer_t in_buf, gss_buffer_t out_buf, + const char **error_r) +{ + struct dsasl_client *client = &gclient->client; + OM_uint32 major_status, minor_status; + OM_uint32 req_flags = GSS_C_REPLAY_FLAG | GSS_C_MUTUAL_FLAG | + GSS_C_SEQUENCE_FLAG | GSS_C_INTEG_FLAG; + gss_OID_desc mech_oid = *auth_gssapi_mech_krb5_oid; + gss_OID ret_mech_oid; + + major_status = gss_init_sec_context(&minor_status, GSS_C_NO_CREDENTIAL, + &gclient->gss_ctx, + gclient->gss_principal, + &mech_oid, req_flags, + 0, GSS_C_NO_CHANNEL_BINDINGS, + in_buf, &ret_mech_oid, out_buf, + NULL, NULL); + if(GSS_ERROR(major_status) != 0) { + (void)gss_release_buffer(&minor_status, out_buf); + *error_r = mech_gssapi_error((in_buf->length == 0 ? + "initializing security context" : + "processing incoming data"), + major_status, minor_status); + return -1; + } + + switch (major_status) { + case GSS_S_COMPLETE: + i_assert(ret_mech_oid != NULL); + if (!auth_gssapi_oid_equal(ret_mech_oid, + auth_gssapi_mech_krb5_oid)) { + *error_r = "GSSAPI mechanism not Kerberos5"; + return -1; + } + e_debug(client->event, + "Security context state completed"); + break; + case GSS_S_CONTINUE_NEEDED: + e_debug(client->event, + "Processed incoming packet correctly, " + "waiting for another"); + return 0; + default: + i_unreached(); + } + + return 1; +} + +static enum dsasl_client_result +mech_gssapi_gs1_sec_context(struct gssapi_sasl_client *gclient, + gss_buffer_t in_buf, const char **error_r) +{ + OM_uint32 minor_status; + gss_buffer_desc out_buf; + int ret; + + ret = mech_gssapi_sec_context(gclient, in_buf, &out_buf, error_r); + if (ret < 0) + return DSASL_CLIENT_RESULT_ERR_PROTOCOL; + + buffer_append(gclient->out_buf, out_buf.value, out_buf.length); + (void)gss_release_buffer(&minor_status, &out_buf); + + if (ret > 0) + gclient->state.gs1 = GSSAPI_GS1_CLIENT_STATE_UNWRAP; + + return DSASL_CLIENT_RESULT_OK; +} + +static enum dsasl_client_result +mech_gssapi_gs1_unwrap(struct gssapi_sasl_client *gclient, + gss_buffer_t in_buf, const char **error_r) +{ + struct dsasl_client *client = &gclient->client; + OM_uint32 major_status, minor_status; + gss_buffer_desc in_buf_wrap = GSS_C_EMPTY_BUFFER; + gss_buffer_desc out_buf = GSS_C_EMPTY_BUFFER; + gss_qop_t qop = GSS_C_QOP_DEFAULT; + unsigned char *data; + unsigned int sec_layer = 0; + unsigned int max_size = 0; + buffer_t *buf; + size_t authzid_size = (client->set.authzid == NULL ? + 0 : strlen(client->set.authzid)); + + /* Decrypt the inbound challenge and obtain the qop */ + major_status = gss_unwrap(&minor_status, gclient->gss_ctx, in_buf, + &out_buf, NULL, &qop); + if(GSS_ERROR(major_status) != 0) { + *error_r = mech_gssapi_error( + "receiving security layer negotiation", + major_status, minor_status); + return DSASL_CLIENT_RESULT_ERR_PROTOCOL; + } + + if (out_buf.length != 4) { + *error_r = "Bad server message: Invalid security data"; + return DSASL_CLIENT_RESULT_ERR_PROTOCOL; + } + + data = out_buf.value; + sec_layer = data[0]; + max_size = (data[1] << 16) | (data[2] << 8) | data[3]; + (void)gss_release_buffer(&minor_status, &out_buf); + + /* Check server parameters */ + if((sec_layer & SASL_GSSAPI_QOP_AUTH_ONLY) == 0) { + *error_r = "Server demands unsupported security parameters"; + return DSASL_CLIENT_RESULT_ERR_PROTOCOL; + } + sec_layer = SASL_GSSAPI_QOP_AUTH_ONLY; + max_size = 0; + + buf = t_buffer_create(4 + authzid_size); + buffer_append_c(buf, sec_layer); + buffer_append_c(buf, max_size >> 16); + buffer_append_c(buf, max_size >> 8); + buffer_append_c(buf, max_size >> 0); + if (client->set.authzid != NULL) + buffer_append(buf, client->set.authzid, authzid_size); + + /* Setup the "authentication data" security buffer */ + in_buf_wrap.value = + buffer_get_modifiable_data(buf, &in_buf_wrap.length); + + /* Encrypt the data */ + major_status = gss_wrap(&minor_status, gclient->gss_ctx, 0, + GSS_C_QOP_DEFAULT, &in_buf_wrap, NULL, + &out_buf); + if (GSS_ERROR(major_status) != 0) { + *error_r = mech_gssapi_error( + "sending security layer negotiation", + major_status, minor_status); + return DSASL_CLIENT_RESULT_ERR_INTERNAL; + } + + e_debug(client->event, "Negotiated security layer"); + + buffer_clear(gclient->out_buf); + buffer_append(gclient->out_buf, out_buf.value, out_buf.length); + (void)gss_release_buffer(&minor_status, &out_buf); + + gclient->state.gs1 = GSSAPI_GS1_CLIENT_STATE_WRAP; + return DSASL_CLIENT_RESULT_OK; +} + +static enum dsasl_client_result +mech_gssapi_gs1_input(struct dsasl_client *client, + const unsigned char *input, size_t input_len, + const char **error_r) +{ + struct gssapi_sasl_client *gclient = + container_of(client, struct gssapi_sasl_client, client); + gss_buffer_desc in_buf; + enum dsasl_client_result result = DSASL_CLIENT_RESULT_ERR_INTERNAL; + + in_buf.value = (void *)input; + in_buf.length = input_len; + + switch (gclient->state.gs1) { + case GSSAPI_GS1_CLIENT_STATE_INIT: + result = mech_gssapi_gs1_init(gclient, &in_buf, error_r); + break; + case GSSAPI_GS1_CLIENT_STATE_SEC_CONTEXT: + result = mech_gssapi_gs1_sec_context(gclient, &in_buf, error_r); + break; + case GSSAPI_GS1_CLIENT_STATE_UNWRAP: + result = mech_gssapi_gs1_unwrap(gclient, &in_buf, error_r); + break; + case GSSAPI_GS1_CLIENT_STATE_WRAP: + case GSSAPI_GS1_CLIENT_STATE_END: + default: + i_unreached(); + } + return result; +} + +static enum dsasl_client_result +mech_gssapi_gs1_output(struct dsasl_client *client, + const unsigned char **output_r, size_t *output_len_r, + const char **error_r) +{ + struct gssapi_sasl_client *gclient = + container_of(client, struct gssapi_sasl_client, client); + gss_buffer_desc in_buf = GSS_C_EMPTY_BUFFER; + enum dsasl_client_result result = DSASL_CLIENT_RESULT_ERR_INTERNAL; + + switch (gclient->state.gs1) { + case GSSAPI_GS1_CLIENT_STATE_INIT: + result = mech_gssapi_gs1_init(gclient, &in_buf, error_r); + if (result != DSASL_CLIENT_RESULT_OK) + return result; + i_assert(gclient->state.gs1 == + GSSAPI_GS1_CLIENT_STATE_SEC_CONTEXT); + result = mech_gssapi_gs1_sec_context(gclient, &in_buf, error_r); + if (result != DSASL_CLIENT_RESULT_OK) + return result; + break; + case GSSAPI_GS1_CLIENT_STATE_SEC_CONTEXT: + case GSSAPI_GS1_CLIENT_STATE_UNWRAP: + case GSSAPI_GS1_CLIENT_STATE_WRAP: + break; + case GSSAPI_GS1_CLIENT_STATE_END: + default: + i_unreached(); + } + + *output_r = gclient->out_buf->data; + *output_len_r = gclient->out_buf->used; + return DSASL_CLIENT_RESULT_OK; +} + +static void mech_gssapi_free(struct dsasl_client *client) +{ + struct gssapi_sasl_client *gclient = + container_of(client, struct gssapi_sasl_client, client); + OM_uint32 minor_status; + + if (gclient->gss_ctx != GSS_C_NO_CONTEXT) { + (void)gss_delete_sec_context(&minor_status, &gclient->gss_ctx, + GSS_C_NO_BUFFER); + } + if (gclient->gss_principal != GSS_C_NO_NAME) + (void)gss_release_name(&minor_status, &gclient->gss_principal); + buffer_free(&gclient->out_buf); +} + +static const struct dsasl_client_mech dsasl_client_mech_gssapi = { + .name = "GSSAPI", + .flags = DSASL_MECH_SEC_ALLOW_NULS, + .struct_size = sizeof(struct gssapi_sasl_client), + + .input = mech_gssapi_gs1_input, + .output = mech_gssapi_gs1_output, + .free = mech_gssapi_free, +}; + +static bool initialized = FALSE; + +void dsasl_clients_init_gssapi(void) +{ + if (initialized) + return; + + initialized = TRUE; + dsasl_client_mech_register(&dsasl_client_mech_gssapi); +} diff --git a/src/lib-sasl/dsasl-client.h b/src/lib-sasl/dsasl-client.h index aa055ec007..d6c384241e 100644 --- a/src/lib-sasl/dsasl-client.h +++ b/src/lib-sasl/dsasl-client.h @@ -83,4 +83,6 @@ int dsasl_client_get_result(struct dsasl_client *client, void dsasl_clients_init(void); void dsasl_clients_deinit(void); +void dsasl_clients_init_gssapi(void); + #endif diff --git a/src/lib-sasl/fuzz-sasl-authentication.c b/src/lib-sasl/fuzz-sasl-authentication.c index ae45db39ad..4d8d5fc77d 100644 --- a/src/lib-sasl/fuzz-sasl-authentication.c +++ b/src/lib-sasl/fuzz-sasl-authentication.c @@ -9,7 +9,9 @@ #include "ioloop.h" #include "istream.h" #include "password-scheme.h" +#include "gssapi-dummy.h" #include "sasl-server.h" +#include "sasl-server-gssapi.h" #include "sasl-server-oauth2.h" #include "dsasl-client.h" #include "dsasl-client-mech-ntlm-dummy.h" @@ -183,17 +185,28 @@ fuzz_server_request_lookup_credentials( container_of(rctx, struct fuzz_sasl_context, ssrctx); struct sasl_passdb_result result; - const struct password_generate_params params = { - .user = fctx->params->authid, - }; - i_zero(&result); + +#ifdef HAVE_GSSAPI + if (strcmp(fctx->params->mech, SASL_MECH_NAME_GSSAPI) == 0) { + i_assert(*scheme == '\0'); + result.status = SASL_PASSDB_RESULT_OK; + callback(&fctx->ssrctx, &result); + return; + } +#endif + if (null_strcmp(fctx->authid, fctx->params->authid) != 0 || null_strcmp(fctx->authzid, fctx->params->authzid) != 0) { result.status = SASL_PASSDB_RESULT_USER_UNKNOWN; callback(&fctx->ssrctx, &result); return; } + + const struct password_generate_params params = { + .user = fctx->params->authid, + }; + if (!password_generate(fctx->params->server_password, ¶ms, scheme, &result.credentials.data, &result.credentials.size)) { @@ -623,6 +636,14 @@ static void fuzz_sasl_run(struct istream *input) winbind_set.helper_path = TEST_WINBIND_HELPER_PATH; sasl_server_mech_register_winbind_ntlm(server_inst, &winbind_set); +#ifdef HAVE_GSSAPI + struct sasl_server_gssapi_settings gssapi_set; + + i_zero(&gssapi_set); + gssapi_set.hostname = "localhost"; + sasl_server_mech_register_gssapi(server_inst, &gssapi_set); +#endif + const struct sasl_server_mech *server_mech; server_mech = sasl_server_mech_find(server_inst, params.mech); @@ -634,6 +655,13 @@ static void fuzz_sasl_run(struct istream *input) e_debug(fuzz_event, "run: %s", str_sanitize(params.mech, 1024)); +#ifdef HAVE_GSSAPI + if (strcmp(params.mech, SASL_MECH_NAME_GSSAPI) == 0) { + gss_dummy_add_principal(params.authid); + gss_dummy_kinit(params.authid); + } +#endif + const struct dsasl_client_mech *client_mech; struct fuzz_sasl_context fctx; @@ -680,6 +708,9 @@ static void fuzz_sasl_run(struct istream *input) } pool_unref(&fctx.pool); +#ifdef HAVE_GSSAPI + gss_dummy_deinit(); +#endif } FUZZ_BEGIN_DATA(const unsigned char *data, size_t size) @@ -690,6 +721,9 @@ FUZZ_BEGIN_DATA(const unsigned char *data, size_t size) password_schemes_init(); dsasl_clients_init(); dsasl_client_mech_ntlm_init_dummy(); +#ifdef HAVE_GSSAPI + dsasl_clients_init_gssapi(); +#endif struct istream *input = i_stream_create_from_data(data, size); struct ioloop *ioloop = io_loop_create(); diff --git a/src/lib-sasl/gssapi-dummy.c b/src/lib-sasl/gssapi-dummy.c new file mode 100644 index 0000000000..85663caac7 --- /dev/null +++ b/src/lib-sasl/gssapi-dummy.c @@ -0,0 +1,514 @@ +/* Copyright (c) 2023 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "buffer.h" +#include "auth-gssapi.h" +#include "gssapi-dummy.h" + +#define GSS_KRB5_DUMMY_FAIL_CODE 1 + +struct gss_name_struct { + char *name; +}; + +struct gss_cred_id_struct { + pool_t pool; + const char *desired_name; +}; + +struct gss_ctx_id_struct { + pool_t pool; + + const char *src_name; + const char *dst_name; + OM_uint32 req_flags; + + gss_OID_desc mech_type; +}; + +gss_OID GSS_C_NT_HOSTBASED_SERVICE = NULL; +const gss_OID GSS_KRB5_NT_PRINCIPAL_NAME = NULL; + +static void gss_alloc_buffer(gss_buffer_t buffer, size_t length); + +char *client_principal = NULL; +char *server_principal = NULL; + +void gss_dummy_kinit(const char *principal) +{ + i_free(client_principal); + client_principal = i_strdup(principal); +} + +void gss_dummy_add_principal(const char *principal) +{ + i_free(server_principal); + server_principal = i_strdup(principal); +} + +void gss_dummy_deinit(void) +{ + i_free(client_principal); + i_free(server_principal); +} + +OM_uint32 KRB5_CALLCONV +gss_acquire_cred(OM_uint32 *minor_status, gss_name_t desired_name, + OM_uint32 time_req, gss_OID_set desired_mechs, + gss_cred_usage_t cred_usage, gss_cred_id_t *output_cred_handle, + gss_OID_set *actual_mechs, OM_uint32 *time_rec) +{ + pool_t pool; + gss_cred_id_t cred_handle; + + i_assert(time_req == 0); // Not implemented + i_assert(desired_mechs == GSS_C_NULL_OID_SET); // Not implemented + i_assert(actual_mechs == NULL); // Not implemented + i_assert(time_rec == NULL); // Not implemented + i_assert(cred_usage == GSS_C_ACCEPT); // Not implemented + + pool = pool_alloconly_create(MEMPOOL_GROWING"gss_cred_id", 256); + cred_handle = p_new(pool, struct gss_cred_id_struct, 1); + cred_handle->pool = pool; + cred_handle->desired_name = p_strdup(pool, desired_name->name); + + *output_cred_handle = cred_handle; + *minor_status = 0; + return 0; +} + +OM_uint32 KRB5_CALLCONV +gss_release_cred(OM_uint32 *minor_status, gss_cred_id_t *cred_handle) +{ + pool_unref(&(*cred_handle)->pool); + *cred_handle = NULL; + + *minor_status = 0; + return 0; +} + +static void _encode_uint32(buffer_t *output, OM_uint32 num) +{ + buffer_append_c(output, (uint8_t)(num >> 24)); + buffer_append_c(output, (uint8_t)(num >> 16)); + buffer_append_c(output, (uint8_t)(num >> 8)); + buffer_append_c(output, (uint8_t)(num >> 0)); +} + +static void _encode_buffer(buffer_t *output, gss_buffer_t buf) +{ + _encode_uint32(output, buf->length); + buffer_append(output, buf->value, buf->length); +} + +OM_uint32 KRB5_CALLCONV +gss_init_sec_context(OM_uint32 *minor_status, + gss_cred_id_t claimant_cred_handle, + gss_ctx_id_t *context_handle, gss_name_t target_name, + gss_OID mech_type, OM_uint32 req_flags, + OM_uint32 time_req, + gss_channel_bindings_t input_chan_bindings, + gss_buffer_t input_token ATTR_UNUSED, + gss_OID *actual_mech_type, + gss_buffer_t output_token, OM_uint32 *ret_flags, + OM_uint32 *time_rec) +{ + pool_t pool; + struct gss_ctx_id_struct *ctx; + + i_assert(claimant_cred_handle == GSS_C_NO_CREDENTIAL); // Not implemented + i_assert(time_req == 0); // Not implemented + i_assert(time_req == 0); // Not implemented + i_assert(ret_flags == NULL); // Not implemented + i_assert(time_rec == NULL); // Not implemented + + pool = pool_alloconly_create(MEMPOOL_GROWING"gss_ctx_id", 256); + ctx = p_new(pool, struct gss_ctx_id_struct, 1); + ctx->pool = pool; + ctx->src_name = p_strdup(pool, client_principal); + ctx->dst_name = p_strdup(pool, target_name->name); + ctx->req_flags = req_flags; + ctx->mech_type = *mech_type; + + size_t src_name_len = strlen(ctx->src_name); + size_t cbind_len = 0; + if (input_chan_bindings != GSS_C_NO_CHANNEL_BINDINGS) { + cbind_len = 4 + 4 + + input_chan_bindings->initiator_address.length + + input_chan_bindings->acceptor_address.length + + input_chan_bindings->application_data.length; + } + buffer_t *output = t_buffer_create( + 4 + ctx->mech_type.length + 4 + src_name_len + cbind_len); + _encode_uint32(output, ctx->mech_type.length); + buffer_append(output, ctx->mech_type.elements, ctx->mech_type.length); + _encode_uint32(output, src_name_len); + buffer_append(output, ctx->src_name, src_name_len); + if (input_chan_bindings != GSS_C_NO_CHANNEL_BINDINGS) { + _encode_uint32(output, input_chan_bindings->initiator_addrtype); + _encode_uint32(output, input_chan_bindings->acceptor_addrtype); + _encode_buffer(output, &input_chan_bindings->initiator_address); + _encode_buffer(output, &input_chan_bindings->acceptor_address); + _encode_buffer(output, &input_chan_bindings->application_data); + } + + gss_alloc_buffer(output_token, output->used); + memcpy(output_token->value, output->data, output->used); + + *context_handle = ctx; + if (actual_mech_type != NULL) + *actual_mech_type = &ctx->mech_type; + + *minor_status = 0; + return 0; +} + +static int +_decode_uint32(const unsigned char **_input, size_t *_input_left, + OM_uint32 *num_r) +{ + const unsigned char *input = *_input; + size_t input_left = *_input_left; + + if (input_left < 4) + return -1; + + *num_r = (OM_uint32)(input[0] << 24) | (OM_uint32)(input[1] << 16) | + (OM_uint32)(input[2] << 8) | (OM_uint32)(input[3] << 0); + + *_input += 4; + *_input_left -= 4; + return 0; +} + +static int +_decode_buffer(const unsigned char **_input, size_t *_input_left, + gss_buffer_t out) +{ + OM_uint32 len; + + if (_decode_uint32(_input, _input_left, &len) < 0) + return -1; + i_zero(out); + if (len > 0) { + out->value = t_memdup_noconst(*_input, len); + out->length = len; + *_input += len; + *_input_left -= len; + } + return 0; +} + +static bool +_gss_buffer_cmp(gss_buffer_t buf1, gss_buffer_t buf2) +{ + if (buf1->length == 0 || buf2->length == 0) + return (buf1->length == buf2->length); + return (memcmp(buf1->value, buf2->value, + I_MIN(buf1->length, buf2->length)) == 0); +} + +static OM_uint32 +gss_parse_sec_context(struct gss_ctx_id_struct *ctx, + gss_buffer_t input_token_buffer, + gss_channel_bindings_t input_chan_bindings) +{ + const unsigned char *input = input_token_buffer->value; + size_t input_left = input_token_buffer->length; + + if (_decode_uint32(&input, &input_left, &ctx->mech_type.length) < 0) + return GSS_S_BAD_MECH; + if (ctx->mech_type.length > input_left) + return GSS_S_BAD_MECH; + ctx->mech_type.elements = p_malloc(ctx->pool, ctx->mech_type.length); + memcpy(ctx->mech_type.elements, input, ctx->mech_type.length); + input += ctx->mech_type.length; + input_left -= ctx->mech_type.length; + + OM_uint32 src_name_len; + if (_decode_uint32(&input, &input_left, &src_name_len) < 0) + return GSS_S_BAD_MECH; + if (input_left < src_name_len) + return GSS_S_BAD_NAME; + ctx->src_name = p_strndup(ctx->pool, (const char *)input, src_name_len); + input += src_name_len; + input_left -= src_name_len; + + if (input_chan_bindings == GSS_C_NO_CHANNEL_BINDINGS) { + if (input_left != 0) + return GSS_S_BAD_MECH; + } else { + OM_uint32 initiator_addrtype; + OM_uint32 acceptor_addrtype; + gss_buffer_desc initiator_address; + gss_buffer_desc acceptor_address; + gss_buffer_desc application_data; + + if (_decode_uint32(&input, &input_left, + &initiator_addrtype) < 0) + return GSS_S_BAD_MECH; + if (_decode_uint32(&input, &input_left, + &acceptor_addrtype) < 0) + return GSS_S_BAD_MECH; + + if (_decode_buffer(&input, &input_left, &initiator_address) < 0) + return GSS_S_BAD_MECH; + if (_decode_buffer(&input, &input_left, &acceptor_address) < 0) + return GSS_S_BAD_MECH; + if (_decode_buffer(&input, &input_left, &application_data) < 0) + return GSS_S_BAD_MECH; + + if (initiator_addrtype != + input_chan_bindings->initiator_addrtype) + return GSS_S_BAD_BINDINGS; + if (acceptor_addrtype != + input_chan_bindings->acceptor_addrtype) + return GSS_S_BAD_BINDINGS; + if (!_gss_buffer_cmp(&initiator_address, + &input_chan_bindings->initiator_address)) + return GSS_S_BAD_BINDINGS; + if (!_gss_buffer_cmp(&acceptor_address, + &input_chan_bindings->acceptor_address)) + return GSS_S_BAD_BINDINGS; + if (!_gss_buffer_cmp(&application_data, + &input_chan_bindings->application_data)) + return GSS_S_BAD_BINDINGS; + } + return 0; +} + +OM_uint32 KRB5_CALLCONV +gss_accept_sec_context(OM_uint32 *minor_status, gss_ctx_id_t *context_handle, + gss_cred_id_t acceptor_cred_handle, + gss_buffer_t input_token_buffer, + gss_channel_bindings_t input_chan_bindings, + gss_name_t *src_name, gss_OID *mech_type, + gss_buffer_t output_token, OM_uint32 *ret_flags, + OM_uint32 *time_rec, + gss_cred_id_t *delegated_cred_handle) +{ + pool_t pool; + struct gss_ctx_id_struct *ctx; + OM_uint32 major_status; + + *minor_status = 0; + + i_assert(ret_flags == NULL); + i_assert(time_rec == NULL); + i_assert(delegated_cred_handle == NULL); + + pool = pool_alloconly_create(MEMPOOL_GROWING"gss_ctx_id", 256); + ctx = p_new(pool, struct gss_ctx_id_struct, 1); + ctx->pool = pool; + ctx->dst_name = p_strdup(pool, acceptor_cred_handle->desired_name); + + major_status = gss_parse_sec_context(ctx, input_token_buffer, + input_chan_bindings); + if (major_status != 0) { + pool_unref(&pool); + return major_status; + } + + if (server_principal == NULL || + strcmp(ctx->src_name, server_principal) != 0) { + pool_unref(&pool); + *minor_status = GSS_KRB5_DUMMY_FAIL_CODE; + return GSS_S_NO_CRED; + } + + struct gss_name_struct *name = i_new(struct gss_name_struct, 1); + name->name = i_strdup(ctx->src_name); + + *context_handle = ctx; + *src_name = name; + *mech_type = &ctx->mech_type; + gss_alloc_buffer(output_token, 0); + + return 0; +} + +OM_uint32 KRB5_CALLCONV +gss_delete_sec_context(OM_uint32 *minor_status, gss_ctx_id_t *context_handle, + gss_buffer_t output_token) +{ + struct gss_ctx_id_struct *ctx = *context_handle; + + i_assert(output_token == GSS_C_NO_BUFFER); + + pool_unref(&ctx->pool); + *context_handle = NULL; + + *minor_status = 0; + return 0; +} + +OM_uint32 KRB5_CALLCONV +gss_wrap(OM_uint32 * minor_status, gss_ctx_id_t context_handle ATTR_UNUSED, + int conf_req_flag, gss_qop_t qop_req, + gss_buffer_t input_message_buffer, int *conf_state, + gss_buffer_t output_message_buffer) +{ + i_assert(conf_req_flag == 0); + i_assert(qop_req == GSS_C_QOP_DEFAULT); + i_assert(conf_state == NULL); + + gss_alloc_buffer(output_message_buffer, input_message_buffer->length); + memcpy(output_message_buffer->value, input_message_buffer->value, + input_message_buffer->length); + + *minor_status = 0; + return 0; +} + +OM_uint32 KRB5_CALLCONV +gss_unwrap(OM_uint32 *minor_status, gss_ctx_id_t context_handle ATTR_UNUSED, + gss_buffer_t input_message_buffer, + gss_buffer_t output_message_buffer, int *conf_state, + gss_qop_t *qop_state ATTR_UNUSED) +{ + i_assert(conf_state == NULL); + + gss_alloc_buffer(output_message_buffer, input_message_buffer->length); + memcpy(output_message_buffer->value, input_message_buffer->value, + input_message_buffer->length); + + *minor_status = 0; + return 0; +} + +OM_uint32 KRB5_CALLCONV +gss_display_status(OM_uint32 *minor_status, + OM_uint32 status_value, + int status_type, gss_OID mech_type ATTR_UNUSED, + OM_uint32 *message_context ATTR_UNUSED, + gss_buffer_t status_string) +{ + char *error_msg = NULL; + + *minor_status = 0; + + switch (status_type) { + case GSS_C_GSS_CODE: + switch (status_value) { + case GSS_S_NO_CRED: + error_msg = i_strdup("No valid credentials."); + break; + default: + break; + } + break; + case GSS_C_MECH_CODE: + switch (status_value) { + case GSS_KRB5_DUMMY_FAIL_CODE: + error_msg = i_strdup("Kerberos5 dummy says no."); + break; + default: + break; + } + break; + default: + break; + } + + if (error_msg == NULL) { + error_msg = i_strdup_printf("STATUS: %"PRIu32":%"PRIu32, + status_type, status_value); + } + + status_string->value = error_msg; + status_string->length = strlen(error_msg); + + return 0; +} + +OM_uint32 KRB5_CALLCONV +gss_compare_name(OM_uint32 *minor_status, gss_name_t name1, gss_name_t name2, + int *name_equal) +{ + *name_equal = (strcmp(name1->name, name2->name) == 0 ? 1 : 0); + + *minor_status = 0; + return 0; +} + +OM_uint32 KRB5_CALLCONV +gss_display_name(OM_uint32 *minor_status, gss_name_t input_name, + gss_buffer_t output_name_buffer, gss_OID *output_name_type) +{ + size_t input_name_len = strlen(input_name->name); + + gss_alloc_buffer(output_name_buffer, input_name_len); + memcpy(output_name_buffer->value, input_name->name, input_name_len); + output_name_buffer->length = input_name_len; + + if (output_name_type != NULL) + *output_name_type = GSS_KRB5_NT_PRINCIPAL_NAME; + + *minor_status = 0; + return 0; +} + +OM_uint32 KRB5_CALLCONV +gss_import_name(OM_uint32 *minor_status, gss_buffer_t input_name_buffer, + gss_OID input_name_type ATTR_UNUSED, gss_name_t *output_name) +{ + struct gss_name_struct *name; + + name = i_new(struct gss_name_struct, 1); + name->name = i_strndup(input_name_buffer->value, + input_name_buffer->length); + *output_name = name; + + *minor_status = 0; + return GSS_S_COMPLETE; +} + +OM_uint32 KRB5_CALLCONV +gss_release_name(OM_uint32 *minor_status, gss_name_t *input_name) +{ + if (*input_name == NULL) { + *minor_status = 0; + return GSS_S_COMPLETE; + } + + i_free((*input_name)->name); + i_free((*input_name)); + + *minor_status = 0; + return GSS_S_COMPLETE; +} + +static void gss_alloc_buffer(gss_buffer_t buffer, size_t length) +{ + i_zero(buffer); + buffer->length = length; + if (length > 0) + buffer->value = i_malloc(length); +} + +OM_uint32 KRB5_CALLCONV +gss_release_buffer(OM_uint32 *minor_status, gss_buffer_t buffer) +{ + if (buffer != NULL) { + if (buffer->value != NULL) + i_free(buffer->value); + i_zero(buffer); + } + + *minor_status = 0; + return GSS_S_COMPLETE; +} + +OM_uint32 KRB5_CALLCONV +gss_duplicate_name(OM_uint32 *minor_status, const gss_name_t input_name, + gss_name_t * dest_name) +{ + struct gss_name_struct *name; + + name = i_new(struct gss_name_struct, 1); + name->name = i_strdup(input_name->name); + *dest_name = name; + + *minor_status = 0; + return GSS_S_COMPLETE; +} diff --git a/src/lib-sasl/gssapi-dummy.h b/src/lib-sasl/gssapi-dummy.h new file mode 100644 index 0000000000..d20e2b963e --- /dev/null +++ b/src/lib-sasl/gssapi-dummy.h @@ -0,0 +1,9 @@ +#ifndef GSSAPI_DUMMY_H +#define GSSAPI_DUMMY_H + +void gss_dummy_kinit(const char *principal); +void gss_dummy_add_principal(const char *principal); + +void gss_dummy_deinit(void); + +#endif diff --git a/src/lib-sasl/krb5-dummy.c b/src/lib-sasl/krb5-dummy.c new file mode 100644 index 0000000000..dc2a9f84b1 --- /dev/null +++ b/src/lib-sasl/krb5-dummy.c @@ -0,0 +1,61 @@ +#include "lib.h" + +#ifndef HAVE___GSS_USEROK +# define USE_KRB5_USEROK +# include + +#ifdef HAVE_GSSAPI_GSSAPI_KRB5_H +# include +#elif defined (HAVE_GSSAPI_KRB5_H) +# include +#else +# undef USE_KRB5_USEROK +#endif + +#ifdef USE_KRB5_USEROK +krb5_error_code KRB5_CALLCONV +krb5_parse_name(krb5_context context ATTR_UNUSED, const char *name ATTR_UNUSED, + krb5_principal *principal_out ATTR_UNUSED) +{ + return 0; +} + +void KRB5_CALLCONV +krb5_free_principal(krb5_context context ATTR_UNUSED, + krb5_principal val ATTR_UNUSED) +{ +} + +krb5_error_code KRB5_CALLCONV +krb5_init_context(krb5_context *context ATTR_UNUSED) +{ + return 0; +} + +void KRB5_CALLCONV krb5_free_context(krb5_context context ATTR_UNUSED) +{ +} + +krb5_boolean KRB5_CALLCONV +krb5_kuserok(krb5_context context ATTR_UNUSED, + krb5_principal principal ATTR_UNUSED, + const char *luser ATTR_UNUSED) +{ + return 0; +} +#endif + +#ifdef HAVE_GSSKRB5_REGISTER_ACCEPTOR_IDENTITY +OM_uint32 gsskrb5_register_acceptor_identity(const char *identity ATTR_UNUSED) +{ + return 0; +} +#elif defined (HAVE_KRB5_GSS_REGISTER_ACCEPTOR_IDENTITY) +OM_uint32 KRB5_CALLCONV +krb5_gss_register_acceptor_identity(const char *path ATTR_UNUSED) +{ + return 0; +} +#endif + +#endif diff --git a/src/lib-sasl/test-sasl-authentication.c b/src/lib-sasl/test-sasl-authentication.c index 054424eea9..96bfbc3ae5 100644 --- a/src/lib-sasl/test-sasl-authentication.c +++ b/src/lib-sasl/test-sasl-authentication.c @@ -7,8 +7,10 @@ #include "randgen.h" #include "test-common.h" #include "password-scheme.h" +#include "gssapi-dummy.h" #include "sasl-server.h" #include "sasl-server-oauth2.h" +#include "sasl-server-gssapi.h" #include "dsasl-client.h" #include "dsasl-client-mech-ntlm-dummy.h" @@ -206,6 +208,15 @@ test_server_request_lookup_credentials( i_zero(&result); +#ifdef HAVE_GSSAPI + if (strcmp(tctx->test->mech, SASL_MECH_NAME_GSSAPI) == 0) { + i_assert(*scheme == '\0'); + result.status = SASL_PASSDB_RESULT_OK; + callback(&tctx->ssrctx, &result); + return; + } +#endif + if (null_strcmp(test->server.authid, tctx->authid) != 0 || null_strcmp(test->server.authzid, tctx->authzid) != 0) { e_debug(test_event, "User unknown"); @@ -512,11 +523,27 @@ test_sasl_run(const struct test_sasl *test, const char *label, }; sasl_server_mech_register_winbind_ntlm(server_inst, &winbind_set); +#ifdef HAVE_GSSAPI + struct sasl_server_gssapi_settings gssapi_set; + + i_zero(&gssapi_set); + gssapi_set.hostname = "localhost"; + sasl_server_mech_register_gssapi(server_inst, &gssapi_set); +#endif + const struct sasl_server_mech *server_mech; server_mech = sasl_server_mech_find(server_inst, test->mech); i_assert(server_mech != NULL); +#ifdef HAVE_GSSAPI + if (strcmp(test->mech, SASL_MECH_NAME_GSSAPI) == 0) { + gss_dummy_add_principal(test->server.authid); + gss_dummy_kinit(test->client.authid != NULL ? + test->client.authid : test->server.authid); + } +#endif + struct test_sasl_passdb passdb; unsigned int repeat = (test->repeat > 0 ? test->repeat : 1); @@ -722,6 +749,17 @@ static const struct test_sasl success_tests[] = { .password = "", }, }, +#ifdef HAVE_GSSAPI + /* GSSAPI */ + { + .mech = "GSSAPI", + .authid_type = SASL_SERVER_AUTHID_TYPE_USERNAME, + .server = { + .authid = "user", + .password = "", + }, + }, +#endif /* NTLM */ { .mech = "NTLM", @@ -1447,6 +1485,21 @@ static const struct test_sasl bad_creds_tests[] = { }, .failure = TRUE, }, +#ifdef HAVE_GSSAPI + /* GSSAPI */ + { + .mech = "GSSAPI", + .authid_type = SASL_SERVER_AUTHID_TYPE_USERNAME, + .server = { + .authid = "user", + .password = "", + }, + .client = { + .authid = "userb", + }, + .failure = TRUE, + }, +#endif /* NTLM */ { .mech = "NTLM", @@ -1529,6 +1582,9 @@ int main(int argc, char *argv[]) password_schemes_init(); dsasl_clients_init(); dsasl_client_mech_ntlm_init_dummy(); +#ifdef HAVE_GSSAPI + dsasl_clients_init_gssapi(); +#endif ret = test_run(test_functions);