add SASL support

patch initially by Jack Stone <jwjstone@fastmail.fm>,
cleaned up by Jan Synacek <jsynacek@redhat.com>,
... and then almost completely rewritten by me. ^^
This commit is contained in:
Oswald Buddenhagen 2014-07-27 18:41:22 +02:00
parent 360600b98d
commit eb1005151c
7 changed files with 287 additions and 15 deletions

2
NEWS
View File

@ -6,7 +6,7 @@ The SSL/TLS configuration has been re-designed.
SSL is now explicitly enabled or disabled - "use SSL if available" is gone. SSL is now explicitly enabled or disabled - "use SSL if available" is gone.
Notice: Tunnels are assumed to be secure and thus default to no SSL. Notice: Tunnels are assumed to be secure and thus default to no SSL.
More flexible configuration of the used authentication mechanism. Support for SASL (flexible authentication) has been added.
[1.1.0] [1.1.0]

2
README
View File

@ -32,7 +32,7 @@ isync executable still exists; it is a compatibility wrapper around mbsync.
* Trash functionality: backup messages before removing them * Trash functionality: backup messages before removing them
* IMAP features: * IMAP features:
* Supports TLS/SSL via imaps: (port 993) and STARTTLS (RFC2595) * Supports TLS/SSL via imaps: (port 993) and STARTTLS (RFC2595)
* Supports CRAM-MD5 (RFC2195) for authentication * Supports SASL (RFC4422) for authentication
* Supports NAMESPACE (RFC2342) for simplified configuration * Supports NAMESPACE (RFC2342) for simplified configuration
* Pipelining for maximum speed * Pipelining for maximum speed

4
TODO
View File

@ -6,10 +6,6 @@ real transactions would be certainly not particularly useful ...
make sync_chans() aware of servers, so a bad server (e.g., wrong password) make sync_chans() aware of servers, so a bad server (e.g., wrong password)
won't cause the same error message for every attached store. won't cause the same error message for every attached store.
add support for more authentication methods: oauth, ntlm, ... use SASL?
possibly by calling an external command. that might be overkill, and
wouldn't be very user-friendly, though.
make SSL (connect) timeouts produce a bit more than "Unidentified socket error". make SSL (connect) timeouts produce a bit more than "Unidentified socket error".
network timeout handling in general would be a good idea. network timeout handling in general would be a good idea.

View File

@ -100,6 +100,45 @@ if test "x$ob_cv_with_ssl" != xno; then
fi fi
AC_SUBST(SSL_LIBS) AC_SUBST(SSL_LIBS)
have_sasl_paths=
AC_ARG_WITH(sasl,
AS_HELP_STRING([--with-sasl[=PATH]], [where to look for SASL [detect]]),
[ob_cv_with_sasl=$withval])
if test "x$ob_cv_with_sasl" != xno; then
case $ob_cv_with_sasl in
""|yes)
dnl FIXME: Try various possible paths here...
;;
*)
SASL_LDFLAGS=-L$ob_cv_with_sasl/lib$libsuff
SASL_CPPFLAGS=-I$ob_cv_with_sasl/include
;;
esac
if test -z "$have_sasl_paths"; then
sav_LDFLAGS=$LDFLAGS
LDFLAGS="$LDFLAGS $SASL_LDFLAGS"
AC_CHECK_LIB(sasl2, sasl_client_init,
[SASL_LIBS="-lsasl2" have_sasl_paths=yes])
LDFLAGS=$sav_LDFLAGS
fi
sav_CPPFLAGS=$CPPFLAGS
CPPFLAGS="$CPPFLAGS $SASL_CPPFLAGS"
AC_CHECK_HEADER(sasl/sasl.h, , [have_sasl_paths=])
CPPFLAGS=$sav_CPPFLAGS
if test -z "$have_sasl_paths"; then
if test -n "$ob_cv_with_sasl"; then
AC_MSG_ERROR([SASL libs and/or includes were not found where specified])
fi
else
AC_DEFINE(HAVE_LIBSASL, 1, [if you have the SASL libraries])
CPPFLAGS="$CPPFLAGS $SASL_CPPFLAGS"
LDFLAGS="$LDFLAGS $SASL_LDFLAGS"
fi
fi
AC_SUBST(SASL_LIBS)
AC_CACHE_CHECK([for Berkley DB >= 4.2], ac_cv_berkdb4, AC_CACHE_CHECK([for Berkley DB >= 4.2], ac_cv_berkdb4,
[ac_cv_berkdb4=no [ac_cv_berkdb4=no
AC_TRY_LINK([#include <db.h>], AC_TRY_LINK([#include <db.h>],
@ -122,12 +161,15 @@ AM_CONDITIONAL(with_compat, test "x$ob_cv_enable_compat" != xno)
AC_CONFIG_FILES([Makefile src/Makefile src/compat/Makefile isync.spec]) AC_CONFIG_FILES([Makefile src/Makefile src/compat/Makefile isync.spec])
AC_OUTPUT AC_OUTPUT
AC_MSG_RESULT()
if test -n "$have_ssl_paths"; then if test -n "$have_ssl_paths"; then
AC_MSG_RESULT([ AC_MSG_RESULT([Using SSL])
Using SSL
])
else else
AC_MSG_RESULT([ AC_MSG_RESULT([Not using SSL])
Not using SSL
])
fi fi
if test -n "$have_sasl_paths"; then
AC_MSG_RESULT([Using SASL])
else
AC_MSG_RESULT([Not using SASL])
fi
AC_MSG_RESULT()

View File

@ -6,7 +6,7 @@ SUBDIRS = $(compat_dir)
bin_PROGRAMS = mbsync mdconvert bin_PROGRAMS = mbsync mdconvert
mbsync_SOURCES = main.c sync.c config.c util.c socket.c driver.c drv_imap.c drv_maildir.c mbsync_SOURCES = main.c sync.c config.c util.c socket.c driver.c drv_imap.c drv_maildir.c
mbsync_LDADD = -ldb $(SSL_LIBS) $(SOCK_LIBS) mbsync_LDADD = -ldb $(SSL_LIBS) $(SOCK_LIBS) $(SASL_LIBS)
noinst_HEADERS = common.h config.h driver.h sync.h socket.h noinst_HEADERS = common.h config.h driver.h sync.h socket.h
mdconvert_SOURCES = mdconvert.c mdconvert_SOURCES = mdconvert.c

View File

@ -36,6 +36,11 @@
#include <time.h> #include <time.h>
#include <sys/wait.h> #include <sys/wait.h>
#ifdef HAVE_LIBSASL
# include <sasl/sasl.h>
# include <sasl/saslutil.h>
#endif
#ifdef HAVE_LIBSSL #ifdef HAVE_LIBSSL
enum { SSL_None, SSL_STARTTLS, SSL_IMAPS }; enum { SSL_None, SSL_STARTTLS, SSL_IMAPS };
#endif #endif
@ -115,6 +120,10 @@ typedef struct imap_store {
void (*imap_cancel)( void *aux ); void (*imap_cancel)( void *aux );
} callbacks; } callbacks;
void *callback_aux; void *callback_aux;
#ifdef HAVE_LIBSASL
sasl_conn_t *sasl;
int sasl_cont;
#endif
conn_t conn; /* this is BIG, so put it last */ conn_t conn; /* this is BIG, so put it last */
} imap_store_t; } imap_store_t;
@ -173,6 +182,9 @@ struct imap_cmd_refcounted {
enum CAPABILITY { enum CAPABILITY {
NOLOGIN = 0, NOLOGIN = 0,
#ifdef HAVE_LIBSASL
SASLIR,
#endif
#ifdef HAVE_LIBSSL #ifdef HAVE_LIBSSL
STARTTLS, STARTTLS,
#endif #endif
@ -184,6 +196,9 @@ enum CAPABILITY {
static const char *cap_list[] = { static const char *cap_list[] = {
"LOGINDISABLED", "LOGINDISABLED",
#ifdef HAVE_LIBSASL
"SASL-IR",
#endif
#ifdef HAVE_LIBSSL #ifdef HAVE_LIBSSL
"STARTTLS", "STARTTLS",
#endif #endif
@ -1354,6 +1369,9 @@ imap_cancel_store( store_t *gctx )
{ {
imap_store_t *ctx = (imap_store_t *)gctx; imap_store_t *ctx = (imap_store_t *)gctx;
#ifdef HAVE_LIBSASL
sasl_dispose( &ctx->sasl );
#endif
socket_close( &ctx->conn ); socket_close( &ctx->conn );
cancel_submitted_imap_cmds( ctx ); cancel_submitted_imap_cmds( ctx );
cancel_pending_imap_cmds( ctx ); cancel_pending_imap_cmds( ctx );
@ -1708,6 +1726,162 @@ ensure_password( imap_server_conf_t *srvc )
return srvc->pass; return srvc->pass;
} }
#ifdef HAVE_LIBSASL
static sasl_callback_t sasl_callbacks[] = {
{ SASL_CB_USER, NULL, NULL },
{ SASL_CB_PASS, NULL, NULL },
{ SASL_CB_LIST_END, NULL, NULL }
};
static int
process_sasl_interact( sasl_interact_t *interact, imap_server_conf_t *srvc )
{
const char *val;
for (;; ++interact) {
switch (interact->id) {
case SASL_CB_LIST_END:
return 0;
case SASL_CB_USER:
val = ensure_user( srvc );
break;
case SASL_CB_PASS:
val = ensure_password( srvc );
break;
default:
error( "Error: Unknown SASL interaction ID\n" );
return -1;
}
if (!val)
return -1;
interact->result = val;
interact->len = strlen( val );
}
}
static int
process_sasl_step( imap_store_t *ctx, int rc, const char *in, unsigned in_len,
sasl_interact_t *interact, const char **out, unsigned *out_len )
{
imap_server_conf_t *srvc = ((imap_store_conf_t *)ctx->gen.conf)->server;
while (rc == SASL_INTERACT) {
if (process_sasl_interact( interact, srvc ) < 0)
return -1;
rc = sasl_client_step( ctx->sasl, in, in_len, &interact, out, out_len );
}
if (rc == SASL_CONTINUE) {
ctx->sasl_cont = 1;
} else if (rc == SASL_OK) {
ctx->sasl_cont = 0;
} else {
error( "Error: %s\n", sasl_errdetail( ctx->sasl ) );
return -1;
}
return 0;
}
static int
decode_sasl_data( const char *prompt, char **in, unsigned *in_len )
{
if (prompt) {
int rc;
unsigned prompt_len = strlen( prompt );
/* We're decoding, the output will be shorter than prompt_len. */
*in = nfmalloc( prompt_len );
rc = sasl_decode64( prompt, prompt_len, *in, prompt_len, in_len );
if (rc != SASL_OK) {
free( *in );
error( "Error: SASL(%d): %s\n", rc, sasl_errstring( rc, NULL, NULL ) );
return -1;
}
} else {
*in = NULL;
*in_len = 0;
}
return 0;
}
static int
encode_sasl_data( const char *out, unsigned out_len, char **enc, unsigned *enc_len )
{
int rc;
unsigned enc_len_max = ((out_len + 2) / 3) * 4 + 1;
*enc = nfmalloc( enc_len_max );
rc = sasl_encode64( out, out_len, *enc, enc_len_max, enc_len );
if (rc != SASL_OK) {
free( *enc );
error( "Error: SASL(%d): %s\n", rc, sasl_errstring( rc, NULL, NULL ) );
return -1;
}
return 0;
}
static int
do_sasl_auth( imap_store_t *ctx, struct imap_cmd *cmdp ATTR_UNUSED, const char *prompt )
{
int rc, ret;
unsigned in_len, out_len, enc_len;
const char *out;
char *in, *enc;
sasl_interact_t *interact = NULL;
if (!ctx->sasl_cont) {
error( "Error: IMAP wants more steps despite successful SASL authentication.\n" );
goto bail;
}
if (decode_sasl_data( prompt, &in, &in_len ) < 0)
goto bail;
rc = sasl_client_step( ctx->sasl, in, in_len, &interact, &out, &out_len );
ret = process_sasl_step( ctx, rc, in, in_len, interact, &out, &out_len );
free( in );
if (ret < 0)
goto bail;
if (out) {
if (encode_sasl_data( out, out_len, &enc, &enc_len ) < 0)
goto bail;
if (DFlags & VERBOSE) {
printf( "%s>+> %s\n", ctx->label, enc );
fflush( stdout );
}
if (socket_write( &ctx->conn, enc, enc_len, GiveOwn ) < 0)
return -1;
} else {
if (DFlags & VERBOSE) {
printf( "%s>+>\n", ctx->label );
fflush( stdout );
}
}
return socket_write( &ctx->conn, "\r\n", 2, KeepOwn );
bail:
imap_open_store_bail( ctx );
return -1;
}
static void
done_sasl_auth( imap_store_t *ctx, struct imap_cmd *cmd ATTR_UNUSED, int response )
{
if (response == RESP_OK && ctx->sasl_cont) {
sasl_interact_t *interact = NULL;
const char *out;
unsigned out_len;
int rc = sasl_client_step( ctx->sasl, NULL, 0, &interact, &out, &out_len );
if (process_sasl_step( ctx, rc, NULL, 0, interact, &out, &out_len ) < 0)
warn( "Warning: SASL reported failure despite successful IMAP authentication. Ignoring...\n" );
else if (out)
warn( "Warning: SASL wants more steps despite successful IMAP authentication. Ignoring...\n" );
}
imap_open_store_authenticate2_p2( ctx, NULL, response );
}
#endif
static void static void
imap_open_store_authenticate2( imap_store_t *ctx ) imap_open_store_authenticate2( imap_store_t *ctx )
{ {
@ -1718,6 +1892,9 @@ imap_open_store_authenticate2( imap_store_t *ctx )
int auth_cram = 0; int auth_cram = 0;
#endif #endif
int auth_login = 0; int auth_login = 0;
#ifdef HAVE_LIBSASL
char saslmechs[1024], *saslend = saslmechs;
#endif
info( "Logging in...\n" ); info( "Logging in...\n" );
for (mech = srvc->auth_mechs; mech; mech = mech->next) { for (mech = srvc->auth_mechs; mech; mech = mech->next) {
@ -1734,12 +1911,70 @@ imap_open_store_authenticate2( imap_store_t *ctx )
auth_cram = 1; auth_cram = 1;
#endif #endif
} else { } else {
#ifdef HAVE_LIBSASL
int len = strlen( cmech->string );
if (saslend + len + 2 > saslmechs + sizeof(saslmechs))
oob();
*saslend++ = ' ';
memcpy( saslend, cmech->string, len + 1 );
saslend += len;
#else
error( "IMAP error: authentication mechanism %s is not supported\n", cmech->string ); error( "IMAP error: authentication mechanism %s is not supported\n", cmech->string );
goto bail; goto bail;
#endif
} }
} }
} }
} }
#ifdef HAVE_LIBSASL
if (saslend != saslmechs) {
int rc;
unsigned out_len = 0;
char *enc = NULL;
const char *gotmech = NULL, *out = NULL;
sasl_interact_t *interact = NULL;
struct imap_cmd *cmd;
static int sasl_inited;
if (!sasl_inited) {
rc = sasl_client_init( sasl_callbacks );
if (rc != SASL_OK) {
saslbail:
error( "Error: SASL(%d): %s\n", rc, sasl_errstring( rc, NULL, NULL ) );
goto bail;
}
sasl_inited = 1;
}
rc = sasl_client_new( "imap", srvc->sconf.host, NULL, NULL, NULL, 0, &ctx->sasl );
if (rc != SASL_OK) {
if (!ctx->sasl)
goto saslbail;
error( "Error: %s\n", sasl_errdetail( ctx->sasl ) );
goto bail;
}
rc = sasl_client_start( ctx->sasl, saslmechs + 1, &interact, CAP(SASLIR) ? &out : NULL, &out_len, &gotmech );
if (gotmech)
info( "Authenticating with SASL mechanism %s...\n", gotmech );
/* Technically, we are supposed to loop over sasl_client_start(),
* but it just calls sasl_client_step() anyway. */
if (process_sasl_step( ctx, rc, NULL, 0, interact, CAP(SASLIR) ? &out : NULL, &out_len ) < 0)
goto bail;
if (out) {
if (!out_len)
enc = nfstrdup( "=" ); /* A zero-length initial response is encoded as padding. */
else if (encode_sasl_data( out, out_len, &enc, NULL ) < 0)
goto bail;
}
cmd = new_imap_cmd( sizeof(*cmd) );
cmd->param.cont = do_sasl_auth;
imap_exec( ctx, cmd, done_sasl_auth, enc ? "AUTHENTICATE %s %s" : "AUTHENTICATE %s", gotmech, enc );
free( enc );
return;
}
#endif
#ifdef HAVE_LIBSSL #ifdef HAVE_LIBSSL
if (auth_cram) { if (auth_cram) {
struct imap_cmd *cmd = new_imap_cmd( sizeof(*cmd) ); struct imap_cmd *cmd = new_imap_cmd( sizeof(*cmd) );

View File

@ -281,8 +281,7 @@ the legacy IMAP \fBLOGIN\fR mechanism is known.
The wildcard \fB*\fR represents all mechanisms that are deemed secure The wildcard \fB*\fR represents all mechanisms that are deemed secure
enough for the current \fBSSLType\fR setting. enough for the current \fBSSLType\fR setting.
The actually used mechanism is the most secure choice from the intersection The actually used mechanism is the most secure choice from the intersection
of this list, the list supplied by the server, and the mechanisms actually of this list, the list supplied by the server, and the installed SASL modules.
supported by \fBmbsync\fR (currently only \fBCRAM-MD5\fR and \fBLOGIN\fR).
(Default: \fB*\fR) (Default: \fB*\fR)
.. ..
.TP .TP