From 14a806d6c13afdfb2f44dce64e50bffa6cb6869c Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Fri, 27 Dec 2019 18:37:19 +0000 Subject: [PATCH] Authenticator gsasl: client support. Bug 2349 --- doc/doc-docbook/spec.xfpt | 28 +- doc/doc-txt/NewStuff | 4 + doc/doc-txt/OptionLists.txt | 5 +- src/src/EDITME | 3 + src/src/auths/get_data.c | 2 +- src/src/auths/gsasl_exim.c | 349 ++++++++++++++++---- src/src/auths/gsasl_exim.h | 7 + src/src/drtables.c | 2 +- src/src/tls-openssl.c | 4 +- test/confs/3820 | 52 ++- test/confs/3821 | 1 + test/confs/3828 | 66 ++++ test/confs/3829 | 1 + test/log/3828 | 12 + test/scripts/3820-Gnu-SASL/3821 | 10 + test/scripts/3828-gsasl-plaintext/3828 | 16 + test/scripts/3828-gsasl-plaintext/REQUIRES | 2 + test/scripts/3829-gsasl-scram-plus/3829 | 8 + test/scripts/3829-gsasl-scram-plus/REQUIRES | 2 + 19 files changed, 497 insertions(+), 77 deletions(-) create mode 120000 test/confs/3821 create mode 100644 test/confs/3828 create mode 120000 test/confs/3829 create mode 100644 test/log/3828 create mode 100644 test/scripts/3820-Gnu-SASL/3821 create mode 100644 test/scripts/3828-gsasl-plaintext/3828 create mode 100644 test/scripts/3828-gsasl-plaintext/REQUIRES create mode 100644 test/scripts/3829-gsasl-scram-plus/3829 create mode 100644 test/scripts/3829-gsasl-scram-plus/REQUIRES diff --git a/doc/doc-docbook/spec.xfpt b/doc/doc-docbook/spec.xfpt index 42a393558..eea304d64 100644 --- a/doc/doc-docbook/spec.xfpt +++ b/doc/doc-docbook/spec.xfpt @@ -27435,19 +27435,37 @@ auth_mechanisms = plain login ntlm .cindex "authentication" "DIGEST-MD5" .cindex "authentication" "CRAM-MD5" .cindex "authentication" "SCRAM-SHA-1" -The &(gsasl)& authenticator provides server integration for the GNU SASL +The &(gsasl)& authenticator provides integration for the GNU SASL library and the mechanisms it provides. This is new as of the 4.80 release and there are a few areas where the library does not let Exim smoothly scale to handle future authentication mechanisms, so no guarantee can be made that any particular new authentication mechanism will be supported without code changes in Exim. -Exim's &(gsasl)& authenticator does not have client-side support at this -time; only the server-side support is implemented. Patches welcome. +.new +.option client_authz gsasl string&!! unset +This option can be used to supply an &'authorization id'& +which is different to the &'authentication_id'& provided +by $%client_username%& option. +If unset or (after expansion) empty it is not used, +which is the common case. + +.option client_channelbinding gsasl boolean false +See $%server_channelbinding%& below. + +.option client_password gsasl string&!! unset +This option is exapanded before use, and should result in +the password to be used, in clear. + +.option client_username gsasl string&!! unset +This option is exapanded before use, and should result in +the account name to be used. +.wen .option server_channelbinding gsasl boolean false -Do not set this true without consulting a cryptographic engineer. +Do not set this true and rely on the properties +without consulting a cryptographic engineer. Some authentication mechanisms are able to use external context at both ends of the session to bind the authentication to that context, and fail the @@ -27469,7 +27487,7 @@ This defaults off to ensure smooth upgrade across Exim releases, in case this option causes some clients to start failing. Some future release of Exim might have switched the default to be true. -However, Channel Binding in TLS has proven to be broken in current versions. +However, Channel Binding in TLS has proven to be vulnerable in current versions. Do not plan to rely upon this feature for security, ever, without consulting with a subject matter expert (a cryptographic engineer). diff --git a/doc/doc-txt/NewStuff b/doc/doc-txt/NewStuff index cd380a3f3..6b163b8b2 100644 --- a/doc/doc-txt/NewStuff +++ b/doc/doc-txt/NewStuff @@ -17,6 +17,10 @@ Version 4.94 3. A msg:defer event. + 4. Client-side support in the gsasl authenticator. Tested against the plaintext + driver for PLAIN; only against itself for SCRAM-SHA-1 and SCRAM-SHA-1-PLUS + methods. + Version 4.93 ------------ diff --git a/doc/doc-txt/OptionLists.txt b/doc/doc-txt/OptionLists.txt index 1618e4279..2978aed35 100644 --- a/doc/doc-txt/OptionLists.txt +++ b/doc/doc-txt/OptionLists.txt @@ -127,12 +127,15 @@ check_spool_space integer 0 main check_string string "From " appendfile 3.03 unset pipe 3.03 check_srv string* unset dnslookup 4.31 +client_authz string* unset gsasl 4.94 client_condition string* unset authenticators 4.68 client_ignore_invalid_base64 boolean false plaintext 4.61 client_name string* + cram_md5 3.10 +client_password string* unset gsasl 4.94 client_secret string* unset cram_md5 3.10 client_send string* unset plaintext 3.10 -client_send string* unset external (auth) 4.93 + unset external (auth) 4.93 +client_username string* unset gsasl 4.94 command string* unset lmtp 3.20 unset pipe unset queryprogram 4.00 diff --git a/src/src/EDITME b/src/src/EDITME index 9024b6f73..352bc7d7a 100644 --- a/src/src/EDITME +++ b/src/src/EDITME @@ -784,6 +784,9 @@ FIXED_NEVER_USERS=root # AUTH_LIBS=-lgsasl # AUTH_LIBS=-lgssapi -lheimntlm -lkrb5 -lhx509 -lcom_err -lhcrypto -lasn1 -lwind -lroken -lcrypt +# If using AUTH_GSASL with SCRAM methods, you should also be defining +# SUPPORT_I18N to get standards-conformant support of utf8 normalization. + #------------------------------------------------------------------------------ # When Exim is decoding MIME "words" in header lines, most commonly for use diff --git a/src/src/auths/get_data.c b/src/src/auths/get_data.c index efb4d6d8b..8a05a82e4 100644 --- a/src/src/auths/get_data.c +++ b/src/src/auths/get_data.c @@ -193,7 +193,7 @@ else has succeeded. There may be more data to send, but is there any point in provoking an error here? */ -if (smtp_read_response(sx, US buffer, buffsize, '2', timeout)) +if (smtp_read_response(sx, buffer, buffsize, '2', timeout)) { *inout = NULL; return OK; diff --git a/src/src/auths/gsasl_exim.c b/src/src/auths/gsasl_exim.c index 614c179b7..db14a40e0 100644 --- a/src/src/auths/gsasl_exim.c +++ b/src/src/auths/gsasl_exim.c @@ -2,6 +2,7 @@ * Exim - an Internet mail transport agent * *************************************************/ +/* Copyright (c) The Exim Maintainers 2019 */ /* Copyright (c) University of Cambridge 1995 - 2018 */ /* See the file NOTICE for conditions of use and distribution. */ @@ -26,6 +27,7 @@ sense in all contexts. For some, we can do checks at init time. */ #include "../exim.h" +#define CHANNELBIND_HACK #ifndef AUTH_GSASL /* dummy function to satisfy compilers when we link in an "empty" file. */ @@ -37,12 +39,26 @@ static void dummy(int x) { dummy2(x-1); } #include #include "gsasl_exim.h" +#ifdef SUPPORT_I18N +# include +#endif + + /* Authenticator-specific options. */ /* I did have server_*_condition options for various mechanisms, but since we only ever handle one mechanism at a time, I didn't see the point in keeping that. In case someone sees a point, I've left the condition_check() API alone. */ optionlist auth_gsasl_options[] = { + { "client_authz", opt_stringptr, + (void *)(offsetof(auth_gsasl_options_block, client_authz)) }, + { "client_channelbinding", opt_bool, + (void *)(offsetof(auth_gsasl_options_block, client_channelbinding)) }, + { "client_password", opt_stringptr, + (void *)(offsetof(auth_gsasl_options_block, client_password)) }, + { "client_username", opt_stringptr, + (void *)(offsetof(auth_gsasl_options_block, client_username)) }, + { "server_channelbinding", opt_bool, (void *)(offsetof(auth_gsasl_options_block, server_channelbinding)) }, { "server_hostname", opt_stringptr, @@ -68,14 +84,10 @@ int auth_gsasl_options_count = /* Defaults for the authenticator-specific options. */ auth_gsasl_options_block auth_gsasl_option_defaults = { - US"smtp", /* server_service */ - US"$primary_hostname", /* server_hostname */ - NULL, /* server_realm */ - NULL, /* server_mech */ - NULL, /* server_password */ - NULL, /* server_scram_iter */ - NULL, /* server_scram_salt */ - FALSE /* server_channelbinding */ + .server_service = US"smtp", + .server_hostname = US"$primary_hostname", + .server_scram_iter = US"4096", + /* all others zero/null */ }; @@ -125,8 +137,8 @@ to be set up. */ void auth_gsasl_init(auth_instance *ablock) { -char *p; -int rc, supported; +static char * once = NULL; +int rc; auth_gsasl_options_block *ob = (auth_gsasl_options_block *)(ablock->options_block); @@ -152,48 +164,55 @@ if (!gsasl_ctx) /* We don't need this except to log it for debugging. */ -if ((rc = gsasl_server_mechlist(gsasl_ctx, &p)) != GSASL_OK) - log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator: " - "failed to retrieve list of mechanisms: %s (%s)", - ablock->name, gsasl_strerror_name(rc), gsasl_strerror(rc)); +HDEBUG(D_auth) if (!once) + { + if ((rc = gsasl_server_mechlist(gsasl_ctx, &once)) != GSASL_OK) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator: " + "failed to retrieve list of mechanisms: %s (%s)", + ablock->name, gsasl_strerror_name(rc), gsasl_strerror(rc)); -HDEBUG(D_auth) debug_printf("GNU SASL supports: %s\n", p); + debug_printf("GNU SASL supports: %s\n", once); + } -supported = gsasl_client_support_p(gsasl_ctx, CCS ob->server_mech); -if (!supported) +if (!gsasl_client_support_p(gsasl_ctx, CCS ob->server_mech)) log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator: " "GNU SASL does not support mechanism \"%s\"", ablock->name, ob->server_mech); +ablock->server = TRUE; + if ( !ablock->server_condition && ( streqic(ob->server_mech, US"EXTERNAL") || streqic(ob->server_mech, US"ANONYMOUS") || streqic(ob->server_mech, US"PLAIN") || streqic(ob->server_mech, US"LOGIN") ) ) - log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator: " - "Need server_condition for %s mechanism", + { + ablock->server = FALSE; + HDEBUG(D_auth) debug_printf("%s authenticator: " + "Need server_condition for %s mechanism\n", ablock->name, ob->server_mech); + } /* This does *not* scale to new SASL mechanisms. Need a better way to ask which properties will be needed. */ if ( !ob->server_realm && streqic(ob->server_mech, US"DIGEST-MD5")) - log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator: " - "Need server_realm for %s mechanism", + { + ablock->server = FALSE; + HDEBUG(D_auth) debug_printf("%s authenticator: " + "Need server_realm for %s mechanism\n", ablock->name, ob->server_mech); + } /* At present, for mechanisms we don't panic on absence of server_condition; need to figure out the most generically correct approach to deciding when it's critical and when it isn't. Eg, for simple validation (PLAIN mechanism, etc) it clearly is critical. - -So don't activate without server_condition, this might be relaxed in the future. */ -if (ablock->server_condition) ablock->server = TRUE; -ablock->client = FALSE; +ablock->client = ob->client_username && ob->client_password; } @@ -207,21 +226,38 @@ int rc = 0; struct callback_exim_state *cb_state = (struct callback_exim_state *)gsasl_session_hook_get(sctx); -HDEBUG(D_auth) - debug_printf("GNU SASL Callback entered, prop=%d (loop prop=%d)\n", - prop, callback_loop); - if (!cb_state) { - HDEBUG(D_auth) debug_printf(" not from our server/client processing.\n"); + HDEBUG(D_auth) debug_printf("gsasl callback (%d) not from our server/client processing\n", prop); +#ifdef CHANNELBIND_HACK + if (prop == GSASL_CB_TLS_UNIQUE) + { + uschar * s; + if ((s = gsasl_callback_hook_get(ctx))) + { + HDEBUG(D_auth) debug_printf("GSASL_CB_TLS_UNIQUE from ctx hook\n"); + gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CS s); + } + else + { + HDEBUG(D_auth) debug_printf("GSASL_CB_TLS_UNIQUE! dummy for now\n"); + gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, ""); + } + return GSASL_OK; + } +#endif return GSASL_NO_CALLBACK; } +HDEBUG(D_auth) + debug_printf("GNU SASL Callback entered, prop=%d (loop prop=%d)\n", + prop, callback_loop); + if (callback_loop > 0) { - /* Most likely is that we were asked for property foo, and to - expand the string we asked for property bar to put into an auth - variable, but property bar is not supplied for this mechanism. */ + /* Most likely is that we were asked for property FOO, and to + expand the string we asked for property BAR to put into an auth + variable, but property BAR is not supplied for this mechanism. */ HDEBUG(D_auth) debug_printf("Loop, asked for property %d while handling property %d\n", prop, callback_loop); @@ -261,9 +297,20 @@ struct callback_exim_state cb_state; int rc, auth_result, exim_error, exim_error_override; HDEBUG(D_auth) - debug_printf("GNU SASL: initialising session for %s, mechanism %s.\n", + debug_printf("GNU SASL: initialising session for %s, mechanism %s\n", ablock->name, ob->server_mech); +#ifndef DISABLE_TLS +# ifdef CHANNELBIND_HACK +/* This is a gross hack to get around the library a) requiring that +c-b was already set, at the _start() call, and b) caching a b64'd +version of the binding then which it never updates. */ + +if (tls_in.channelbinding && ob->server_channelbinding) + gsasl_callback_hook_set(gsasl_ctx, tls_in.channelbinding); +# endif +#endif + if ((rc = gsasl_server_start(gsasl_ctx, CCS ob->server_mech, &sctx)) != GSASL_OK) { auth_defer_msg = string_sprintf("GNU SASL: session start failure: %s (%s)", @@ -273,10 +320,9 @@ if ((rc = gsasl_server_start(gsasl_ctx, CCS ob->server_mech, &sctx)) != GSASL_OK } /* Hereafter: gsasl_finish(sctx) please */ -gsasl_session_hook_set(sctx, (void *)ablock); cb_state.ablock = ablock; cb_state.currently = CURRENTLY_SERVER; -gsasl_session_hook_set(sctx, (void *)&cb_state); +gsasl_session_hook_set(sctx, &cb_state); tmps = CS expand_string(ob->server_service); gsasl_property_set(sctx, GSASL_SERVICE, tmps); @@ -316,8 +362,9 @@ if (tls_in.channelbinding) { HDEBUG(D_auth) debug_printf("Auth %s: Enabling channel-binding\n", ablock->name); - gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, - CCS tls_in.channelbinding); +# ifdef CHANNELBIND_HACK + gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CCS tls_in.channelbinding); +# endif } else HDEBUG(D_auth) @@ -375,7 +422,7 @@ do { } if ((rc == GSASL_NEEDS_MORE) || (to_send && *to_send)) - exim_error = auth_get_no64_data((uschar **)&received, US to_send); + exim_error = auth_get_no64_data(USS &received, US to_send); if (to_send) { @@ -454,12 +501,13 @@ expand_nmax = 0; switch (prop) { case GSASL_VALIDATE_SIMPLE: + HDEBUG(D_auth) debug_printf(" VALIDATE_SIMPLE\n"); /* GSASL_AUTHID, GSASL_AUTHZID, and GSASL_PASSWORD */ - propval = US gsasl_property_fast(sctx, GSASL_AUTHID); + propval = US gsasl_property_fast(sctx, GSASL_AUTHID); auth_vars[0] = expand_nstring[1] = propval ? string_copy(propval) : US""; - propval = US gsasl_property_fast(sctx, GSASL_AUTHZID); + propval = US gsasl_property_fast(sctx, GSASL_AUTHZID); auth_vars[1] = expand_nstring[2] = propval ? string_copy(propval) : US""; - propval = US gsasl_property_fast(sctx, GSASL_PASSWORD); + propval = US gsasl_property_fast(sctx, GSASL_PASSWORD); auth_vars[2] = expand_nstring[3] = propval ? string_copy(propval) : US""; expand_nmax = 3; for (int i = 1; i <= 3; ++i) @@ -470,13 +518,14 @@ switch (prop) break; case GSASL_VALIDATE_EXTERNAL: + HDEBUG(D_auth) debug_printf(" VALIDATE_EXTERNAL\n"); if (!ablock->server_condition) { - HDEBUG(D_auth) debug_printf("No server_condition supplied, to validate EXTERNAL.\n"); + HDEBUG(D_auth) debug_printf("No server_condition supplied, to validate EXTERNAL\n"); cbrc = GSASL_AUTHENTICATION_ERROR; break; } - propval = US gsasl_property_fast(sctx, GSASL_AUTHZID); + propval = US gsasl_property_fast(sctx, GSASL_AUTHZID); /* We always set $auth1, even if only to empty string. */ auth_vars[0] = expand_nstring[1] = propval ? string_copy(propval) : US""; @@ -489,13 +538,14 @@ switch (prop) break; case GSASL_VALIDATE_ANONYMOUS: + HDEBUG(D_auth) debug_printf(" VALIDATE_ANONYMOUS\n"); if (!ablock->server_condition) { - HDEBUG(D_auth) debug_printf("No server_condition supplied, to validate ANONYMOUS.\n"); + HDEBUG(D_auth) debug_printf("No server_condition supplied, to validate ANONYMOUS\n"); cbrc = GSASL_AUTHENTICATION_ERROR; break; } - propval = US gsasl_property_fast(sctx, GSASL_ANONYMOUS_TOKEN); + propval = US gsasl_property_fast(sctx, GSASL_ANONYMOUS_TOKEN); /* We always set $auth1, even if only to empty string. */ @@ -509,6 +559,7 @@ switch (prop) break; case GSASL_VALIDATE_GSSAPI: + HDEBUG(D_auth) debug_printf(" VALIDATE_GSSAPI\n"); /* GSASL_AUTHZID and GSASL_GSSAPI_DISPLAY_NAME The display-name is authenticated as part of GSS, the authzid is claimed by the SASL integration after authentication; protected against tampering @@ -518,9 +569,9 @@ switch (prop) to the first release of Exim with this authenticator, they've been switched to match the ordering of GSASL_VALIDATE_SIMPLE. */ - propval = US gsasl_property_fast(sctx, GSASL_GSSAPI_DISPLAY_NAME); + propval = US gsasl_property_fast(sctx, GSASL_GSSAPI_DISPLAY_NAME); auth_vars[0] = expand_nstring[1] = propval ? string_copy(propval) : US""; - propval = US gsasl_property_fast(sctx, GSASL_AUTHZID); + propval = US gsasl_property_fast(sctx, GSASL_AUTHZID); auth_vars[1] = expand_nstring[2] = propval ? string_copy(propval) : US""; expand_nmax = 2; for (int i = 1; i <= 2; ++i) @@ -535,6 +586,7 @@ switch (prop) break; case GSASL_SCRAM_ITER: + HDEBUG(D_auth) debug_printf(" SCRAM_ITER\n"); if (ob->server_scram_iter) { tmps = CS expand_string(ob->server_scram_iter); @@ -544,6 +596,7 @@ switch (prop) break; case GSASL_SCRAM_SALT: + HDEBUG(D_auth) debug_printf(" SCRAM_SALT\n"); if (ob->server_scram_iter) { tmps = CS expand_string(ob->server_scram_salt); @@ -553,6 +606,7 @@ switch (prop) break; case GSASL_PASSWORD: + HDEBUG(D_auth) debug_printf(" PASSWORD\n"); /* DIGEST-MD5: GSASL_AUTHID, GSASL_AUTHZID and GSASL_REALM CRAM-MD5: GSASL_AUTHID PLAIN: GSASL_AUTHID and GSASL_AUTHZID @@ -576,11 +630,11 @@ switch (prop) needing to add more glue, since avoiding that is a large part of the point of SASL. */ - propval = US gsasl_property_fast(sctx, GSASL_AUTHID); + propval = US gsasl_property_fast(sctx, GSASL_AUTHID); auth_vars[0] = expand_nstring[1] = propval ? string_copy(propval) : US""; - propval = US gsasl_property_fast(sctx, GSASL_AUTHZID); + propval = US gsasl_property_fast(sctx, GSASL_AUTHZID); auth_vars[1] = expand_nstring[2] = propval ? string_copy(propval) : US""; - propval = US gsasl_property_fast(sctx, GSASL_REALM); + propval = US gsasl_property_fast(sctx, GSASL_REALM); auth_vars[2] = expand_nstring[3] = propval ? string_copy(propval) : US""; expand_nmax = 3; for (int i = 1; i <= 3; ++i) @@ -604,7 +658,7 @@ switch (prop) break; default: - HDEBUG(D_auth) debug_printf("Unrecognised callback: %d\n", prop); + HDEBUG(D_auth) debug_printf(" Unrecognised callback: %d\n", prop); cbrc = GSASL_NO_CALLBACK; } @@ -615,6 +669,48 @@ return cbrc; } +/******************************************************************************/ + +#define PROP_OPTIONAL BIT(0) +#define PROP_STRINGPREP BIT(1) + + +static BOOL +client_prop(Gsasl_session * sctx, Gsasl_property propnum, uschar * val, + const uschar * why, unsigned flags, uschar * buffer, int buffsize) +{ +uschar * s, * t; +int rc; + +if (flags & PROP_OPTIONAL && !val) return TRUE; +if (!(s = expand_string(val)) || !(flags & PROP_OPTIONAL) && !*s) + { + string_format(buffer, buffsize, "%s", expand_string_message); + return FALSE; + } +if (!*s) return TRUE; + +#ifdef SUPPORT_I18N +if (flags & PROP_STRINGPREP) + { + if (gsasl_saslprep(CCS s, 0, CSS &t, &rc) != GSASL_OK) + { + string_format(buffer, buffsize, "Bad result from saslprep(%s): %s\n", + why, stringprep_strerror(rc)); + HDEBUG(D_auth) debug_printf("%s\n", buffer); + return FALSE; + } + gsasl_property_set(sctx, propnum, CS t); + + free(t); + } +else +#endif + gsasl_property_set(sctx, propnum, CS s); + +return TRUE; +} + /************************************************* * Client entry point * *************************************************/ @@ -629,24 +725,149 @@ auth_gsasl_client( uschar *buffer, /* buffer for reading response */ int buffsize) /* size of buffer */ { +auth_gsasl_options_block *ob = + (auth_gsasl_options_block *)(ablock->options_block); +Gsasl_session * sctx = NULL; +struct callback_exim_state cb_state; +uschar * s; +BOOL initial = TRUE, do_stringprep; +int rc, yield = FAIL, flags; + HDEBUG(D_auth) - debug_printf("Client side NOT IMPLEMENTED: you should not see this!\n"); -/* NOT IMPLEMENTED */ -return FAIL; + debug_printf("GNU SASL: initialising session for %s, mechanism %s\n", + ablock->name, ob->server_mech); + +*buffer = 0; + +#ifndef DISABLE_TLS +/* This is a gross hack to get around the library a) requiring that +c-b was already set, at the _start() call, and b) caching a b64'd +version of the binding then which it never updates. */ + +if (tls_out.channelbinding) + if (ob->client_channelbinding) + gsasl_callback_hook_set(gsasl_ctx, tls_out.channelbinding); +#endif + +if ((rc = gsasl_client_start(gsasl_ctx, CCS ob->server_mech, &sctx)) != GSASL_OK) + { + string_format(buffer, buffsize, "GNU SASL: session start failure: %s (%s)", + gsasl_strerror_name(rc), gsasl_strerror(rc)); + HDEBUG(D_auth) debug_printf("%s\n", buffer); + return ERROR; + } + +cb_state.ablock = ablock; +cb_state.currently = CURRENTLY_CLIENT; +gsasl_session_hook_set(sctx, &cb_state); + +/* Set properties */ + +flags = Ustrncmp(ob->server_mech, "SCRAM-", 5) == 0 ? PROP_STRINGPREP : 0; + +if ( !client_prop(sctx, GSASL_PASSWORD, ob->client_password, US"password", + flags, buffer, buffsize) + || !client_prop(sctx, GSASL_AUTHID, ob->client_username, US"username", + flags, buffer, buffsize) + || !client_prop(sctx, GSASL_AUTHZID, ob->client_authz, US"authz", + flags | PROP_OPTIONAL, buffer, buffsize) + ) + return ERROR; + +#ifndef DISABLE_TLS +if (tls_out.channelbinding) + if (ob->client_channelbinding) + { + HDEBUG(D_auth) debug_printf("Auth %s: Enabling channel-binding\n", + ablock->name); +# ifdef CHANNELBIND_HACK + gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CCS tls_out.channelbinding); +# endif + } + else + HDEBUG(D_auth) + debug_printf("Auth %s: Not enabling channel-binding (data available)\n", + ablock->name); +#endif + +/* Run the SASL conversation with the server */ + +for(s = NULL; ;) + { + uschar * outstr; + BOOL fail; + + rc = gsasl_step64(sctx, CS s, CSS &outstr); + + fail = initial + ? smtp_write_command(sx, SCMD_FLUSH, + outstr ? "AUTH %s %s\r\n" : "AUTH %s\r\n", + ablock->public_name, outstr) <= 0 + : outstr + ? smtp_write_command(sx, SCMD_FLUSH, "%s\r\n", outstr) <= 0 + : FALSE; + if (outstr && *outstr) free(outstr); + if (fail) + { + yield = FAIL_SEND; + goto done; + } + initial = FALSE; + + if (rc != GSASL_NEEDS_MORE) + { + if (rc != GSASL_OK) + { + string_format(buffer, buffsize, "gsasl: %s", gsasl_strerror(rc)); + break; + } + + /* expecting a final 2xx from the server, accepting the AUTH */ + + if (smtp_read_response(sx, buffer, buffsize, '2', timeout)) + yield = OK; + break; /* from SASL sequence loop */ + } + + /* 2xx or 3xx response is acceptable. If 2xx, no further input */ + + if (!smtp_read_response(sx, buffer, buffsize, '3', timeout)) + if (errno == 0 && buffer[0] == '2') + buffer[4] = '\0'; + else + { + yield = FAIL; + goto done; + } + s = buffer + 4; + } + +done: +gsasl_finish(sctx); +return yield; } static int client_callback(Gsasl *ctx, Gsasl_session *sctx, Gsasl_property prop, auth_instance *ablock) { -int cbrc = GSASL_NO_CALLBACK; -HDEBUG(D_auth) - debug_printf("GNU SASL callback %d for %s/%s as client\n", - prop, ablock->name, ablock->public_name); - -HDEBUG(D_auth) - debug_printf("Client side NOT IMPLEMENTED: you should not see this!\n"); - -return cbrc; +HDEBUG(D_auth) debug_printf("GNU SASL callback %d for %s/%s as client\n", + prop, ablock->name, ablock->public_name); +switch (prop) + { + case GSASL_AUTHZID: + HDEBUG(D_auth) debug_printf(" inquired for AUTHZID; not providing one\n"); + break; + case GSASL_SCRAM_SALTED_PASSWORD: + HDEBUG(D_auth) + debug_printf(" inquired for SCRAM_SALTED_PASSWORD; not providing one\n"); + break; + case GSASL_CB_TLS_UNIQUE: + HDEBUG(D_auth) + debug_printf(" inquired for CB_TLS_UNIQUE, filling in\n"); + gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CCS tls_out.channelbinding); + break; + } +return GSASL_NO_CALLBACK; } /************************************************* diff --git a/src/src/auths/gsasl_exim.h b/src/src/auths/gsasl_exim.h index 8842165af..93d20784f 100644 --- a/src/src/auths/gsasl_exim.h +++ b/src/src/auths/gsasl_exim.h @@ -3,6 +3,7 @@ *************************************************/ /* Copyright (c) University of Cambridge 1995 - 2012 */ +/* Copyright (c) The Exim Maintainers 2019 */ /* See the file NOTICE for conditions of use and distribution. */ /* Copyright (c) Twitter Inc 2012 */ @@ -19,7 +20,13 @@ typedef struct { uschar *server_password; uschar *server_scram_iter; uschar *server_scram_salt; + + uschar *client_username; + uschar *client_password; + uschar *client_authz; + BOOL server_channelbinding; + BOOL client_channelbinding; } auth_gsasl_options_block; /* Data for reading the authenticator-specific options. */ diff --git a/src/src/drtables.c b/src/src/drtables.c index 059756284..f2022880b 100644 --- a/src/src/drtables.c +++ b/src/src/drtables.c @@ -128,7 +128,7 @@ auth_info auths_available[] = { .options_len = sizeof(auth_gsasl_options_block), .init = auth_gsasl_init, .servercode = auth_gsasl_server, - .clientcode = NULL, + .clientcode = auth_gsasl_client, .version_report = auth_gsasl_version_report }, #endif diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c index a236bc0c6..bee5a4256 100644 --- a/src/src/tls-openssl.c +++ b/src/src/tls-openssl.c @@ -2831,7 +2831,7 @@ See description in https://paquier.xyz/postgresql-2/channel-binding-openssl/ */ store_pool = POOL_PERM; tls_in.channelbinding = b64encode_taint(CUS s, (int)len, FALSE); store_pool = old_pool; - DEBUG(D_tls) debug_printf("Have channel bindings cached for possible auth usage\n"); + DEBUG(D_tls) debug_printf("Have channel bindings cached for possible auth usage %p\n", tls_in.channelbinding); } /* Only used by the server-side tls (tls_in), including tls_getc. @@ -3407,7 +3407,7 @@ tlsp->cipher_stdname = cipher_stdname_ssl(exim_client_ctx->ssl); store_pool = POOL_PERM; tlsp->channelbinding = b64encode_taint(CUS s, (int)len, TRUE); store_pool = old_pool; - DEBUG(D_tls) debug_printf("Have channel bindings cached for possible auth usage\n"); + DEBUG(D_tls) debug_printf("Have channel bindings cached for possible auth usage %p %p\n", tlsp->channelbinding, tlsp); } tlsp->active.sock = cctx->sock; diff --git a/test/confs/3820 b/test/confs/3820 index a0206f3a0..023ed751d 100644 --- a/test/confs/3820 +++ b/test/confs/3820 @@ -2,17 +2,47 @@ SERVER= +.ifdef TRUSTED +.include DIR/aux-var/tls_conf_prefix +.else .include DIR/aux-var/std_conf_prefix +.endif primary_hostname = myhost.test.ex +tls_certificate = ${if eq {SERVER}{server}{DIR/aux-fixed/cert1}fail} # ----- Main settings ----- +acl_smtp_rcpt = accept +queue_only + + +begin routers + +client_r: + driver = accept + condition = ${if !eq {SERVER}{server}} + transport = smtp + +begin transports + +smtp: + driver = smtp + hosts = 127.0.0.1 + allow_localhost + port = PORT_D +.ifdef TRUSTED + hosts_require_tls = * + tls_verify_certificates = DIR/aux-fixed/cert1 + tls_verify_cert_hostnames = : +.endif + hosts_require_auth = * # ----- Authentication ----- begin authenticators +.ifndef TRUSTED sasl1: driver = gsasl public_name = ANONYMOUS @@ -23,11 +53,22 @@ sasl2: driver = gsasl public_name = PLAIN server_set_id = $auth1 - server_condition = false + server_condition = ${if eq {$auth3}{pencil}} + + client_condition = ${if eq {plain}{$local_part}} + client_username = ph10 + client_password = pencil +.endif sasl3: driver = gsasl +.ifdef TRUSTED + public_name = SCRAM-SHA-1-PLUS + server_advertise_condition = ${if def:tls_in_cipher} + server_channelbinding = true +.else public_name = SCRAM-SHA-1 +.endif # will need to give library salt, stored-key, server-key, itercount # @@ -35,13 +76,18 @@ sasl3: # gsasl takes props: GSASL_SCRAM_ITER, GSASL_SCRAM_SALT. It _might_ take # a GSASL_SCRAM_SALTED_PASSWORD - but that is only documented for client mode. - server_scram_iter = 4096 # unclear if the salt is given in binary or base64 to the library server_scram_salt = QSXCR+Q6sek8bf92 server_password = pencil - server_condition = true server_set_id = $auth1 + client_condition = ${if eq {scram_sha_1}{$local_part}} + client_username = ph10 + client_password = pencil +.ifdef TRUSTED + client_channelbinding = true +.endif + # End diff --git a/test/confs/3821 b/test/confs/3821 new file mode 120000 index 000000000..d8f3286c4 --- /dev/null +++ b/test/confs/3821 @@ -0,0 +1 @@ +3820 \ No newline at end of file diff --git a/test/confs/3828 b/test/confs/3828 new file mode 100644 index 000000000..aa9db9467 --- /dev/null +++ b/test/confs/3828 @@ -0,0 +1,66 @@ +# Exim test configuration 3828 + +SERVER= + +.include DIR/aux-var/std_conf_prefix + +primary_hostname = myhost.test.ex + +# ----- Main settings ----- + +acl_smtp_rcpt = accept +queue_only + + +begin routers + +client_r: + driver = accept + condition = ${if !eq {SERVER}{server}} + transport = smtp + +begin transports + +smtp: + driver = smtp + hosts = 127.0.0.1 + allow_localhost + port = PORT_D + hosts_require_auth = * + +# ----- Authentication ----- + +begin authenticators + +.ifndef OPT +sasl1: + driver = plaintext + public_name = PLAIN + server_prompts = : + server_condition = ${if and {{eq{$auth2}{ph10}}{eq{$auth3}{mysecret}}}} + server_set_id = $auth2 + +sasl2: + driver = gsasl + public_name = PLAIN + client_condition = ${if eq {plain}{$local_part}} + client_username = ph10 + client_password = mysecret + +.else +sasl3: + driver = gsasl + public_name = PLAIN + server_condition = ${if and {{eq{$auth1}{ph10}}{eq{$auth3}{mysecret}}}} + server_set_id = $auth1 + +sasl4: + driver = plaintext + public_name = PLAIN + client_condition = ${if eq {plain}{$local_part}} + client_send = ^ph10^mysecret + +.endif + + +# End diff --git a/test/confs/3829 b/test/confs/3829 new file mode 120000 index 000000000..d8f3286c4 --- /dev/null +++ b/test/confs/3829 @@ -0,0 +1 @@ +3820 \ No newline at end of file diff --git a/test/log/3828 b/test/log/3828 new file mode 100644 index 000000000..038a795d7 --- /dev/null +++ b/test/log/3828 @@ -0,0 +1,12 @@ +1999-03-02 09:44:33 10HmaX-0005vi-00 <= CALLER@myhost.test.ex U=CALLER P=local S=sss +1999-03-02 09:44:33 10HmaX-0005vi-00 => plain@test.ex R=client_r T=smtp H=127.0.0.1 [127.0.0.1] A=sasl2 C="250 OK id=10HmaY-0005vi-00" +1999-03-02 09:44:33 10HmaX-0005vi-00 Completed +1999-03-02 09:44:33 10HmaZ-0005vi-00 <= CALLER@myhost.test.ex U=CALLER P=local S=sss +1999-03-02 09:44:33 10HmaZ-0005vi-00 => plain@test.ex R=client_r T=smtp H=127.0.0.1 [127.0.0.1] A=sasl4 C="250 OK id=10HmbA-0005vi-00" +1999-03-02 09:44:33 10HmaZ-0005vi-00 Completed + +******** SERVER ******** +1999-03-02 09:44:33 exim x.yz daemon started: pid=pppp, no queue runs, listening for SMTP on port PORT_D +1999-03-02 09:44:33 10HmaY-0005vi-00 <= CALLER@myhost.test.ex H=localhost (myhost.test.ex) [127.0.0.1] P=esmtpa A=sasl1:ph10 S=sss id=E10HmaX-0005vi-00@myhost.test.ex +1999-03-02 09:44:33 exim x.yz daemon started: pid=pppp, no queue runs, listening for SMTP on port PORT_D +1999-03-02 09:44:33 10HmbA-0005vi-00 <= CALLER@myhost.test.ex H=localhost (myhost.test.ex) [127.0.0.1] P=esmtpa A=sasl3:ph10 S=sss id=E10HmaZ-0005vi-00@myhost.test.ex diff --git a/test/scripts/3820-Gnu-SASL/3821 b/test/scripts/3820-Gnu-SASL/3821 new file mode 100644 index 000000000..e43f4765a --- /dev/null +++ b/test/scripts/3820-Gnu-SASL/3821 @@ -0,0 +1,10 @@ +# GSASL PLAIN & SCRAM authentication - gsasl client versus gsasl server +# +exim -DSERVER=server -bd -oX PORT_D +**** +exim -odi plain@test.ex +**** +exim -odi scram_sha_1@test.ex +**** +killdaemon +no_msglog_check diff --git a/test/scripts/3828-gsasl-plaintext/3828 b/test/scripts/3828-gsasl-plaintext/3828 new file mode 100644 index 000000000..a30888f3d --- /dev/null +++ b/test/scripts/3828-gsasl-plaintext/3828 @@ -0,0 +1,16 @@ +# GSASL PLAIN authentication: gsasl driver vs. plaintext driver +# +# gsasl client against plaintext server +exim -DSERVER=server -bd -oX PORT_D +**** +exim -odi plain@test.ex +**** +killdaemon +# +# plaintext client against gsasl server +exim -DSERVER=server -DOPT=y -bd -oX PORT_D +**** +exim -odi -DOPT=y plain@test.ex +**** +killdaemon +no_msglog_check diff --git a/test/scripts/3828-gsasl-plaintext/REQUIRES b/test/scripts/3828-gsasl-plaintext/REQUIRES new file mode 100644 index 000000000..905a62278 --- /dev/null +++ b/test/scripts/3828-gsasl-plaintext/REQUIRES @@ -0,0 +1,2 @@ +authenticator gsasl +authenticator plaintext diff --git a/test/scripts/3829-gsasl-scram-plus/3829 b/test/scripts/3829-gsasl-scram-plus/3829 new file mode 100644 index 000000000..8938b1f42 --- /dev/null +++ b/test/scripts/3829-gsasl-scram-plus/3829 @@ -0,0 +1,8 @@ +# GSASL SCRAM-SHA-1-PLUS +# +exim -DSERVER=server -DTRUSTED -bd -oX PORT_D +**** +exim -odi -DTRUSTED scram_sha_1@test.ex +**** +killdaemon +no_msglog_check diff --git a/test/scripts/3829-gsasl-scram-plus/REQUIRES b/test/scripts/3829-gsasl-scram-plus/REQUIRES new file mode 100644 index 000000000..9c2ca0551 --- /dev/null +++ b/test/scripts/3829-gsasl-scram-plus/REQUIRES @@ -0,0 +1,2 @@ +authenticator gsasl +feature _HAVE_TLS -- 2.25.1