Subject: [Wocky] Jabber Kerberos patches

From: Greg Hudson <[email-masked]>
Date: Tue, 2 Aug 2005 12:06:07 -0400

MIT is gearing up for a pilot introduction of a Jabber service, and we
were highly encouraged to see your July 7 post about Kerberos support
in jabberd2 and several Jabber clients
(http://mail.jabber.org/pipermail/jabberd/2005-July/002797.html).  We
were planning to do essentially the same thing ourselves, and this
should dramatically reduce the cost of doing so.

Can we get ahold of your patches for Gaim?  Our current plan is to
recommend Gaim on Linux and Solaris, Adium X on MacOS X, and Exodus on
Windows, as those appeared to be the best-of-breed Jabber clients on
those platforms.  It looks like only Gaim is on your hit list, but
that's still a good start.

We'll also likely be interested in testing server-to-server Kerberos
support, although that's less of a priority for us.

Thanks for your work on this.  It's always great from an MIT
perspective to see application support for Kerberos authentication
coming from outside of MIT.

Greg Hudson
Operations and Infrastructure Services
Information Services and Technology

---

From: Ken Raeburn <[email-masked]>
Date: Tue, 2 Aug 2005 18:28:18 +0200

> that's still a good start.

Adium X appears to be based on the Gaim library, so unless they've  
actually forked, the support should come through eventually, and we  
might be able to help that along... (and I had a couple of things I  
wanted to tweak in Adium anyways, hmm...)

Ken

---

From: Simon Wilkinson <[email-masked]>
Date: Wed, 03 Aug 2005 00:53:31 +0100

Greg Hudson wrote:
> Can we get ahold of your patches for Gaim? 

Certainly. I've attached them to this email. They're also in AFS at
/afs/inf.ed.ac.uk/user/s/sxw/Development/Jabber/Gaim - although our AFS
 is in prototype form at the moment ... The 1.3.1 patch also applies to
1.4.0

> Our current plan is to

I'll be attacking Adium just as soon as my new Mac arrives, hopefully
within the next week or so. My hope is that as Adium is built on
libgaim, it should be fairly simple to port the Gaim patches over.

I took a look at Exodus, but its written in Delphi, which I don't have
immediate access to a development environment for.

> support, although that's less of a priority for us.

Yes - Our (Informatics at Edinburgh University) interest in this is a
fairly low priority item, too.

> coming from outside of MIT.

That's OK. I've had a remit for a few years now to look at better
integrating applications with our Kerberos infrastructure - which has
led to the Kerberos support in OpenSSH, the Jabber code, and (shortly)
GSSAPI support in Thunderbird. It's good to see other people interested
in the code.

Please keep me posted with how you're getting on.

Cheers,

Simon.
diff -ur --exclude aclocal.m4 --exclude config.h --exclude config.h.in --exclude configure --exclude Makefile.in gaim-1.3.1/configure.ac gaim-1.3.1-cyrus/configure.ac
--- gaim-1.3.1/configure.ac	2005-06-10 03:28:13.000000000 +0100
+++ gaim-1.3.1-cyrus/configure.ac	2005-07-01 01:51:07.000000000 +0100
@@ -1070,6 +1070,11 @@
 dnl checks for jabber
 dnl AC_CHECK_SIZEOF(short)
 AC_CHECK_FUNCS(snprintf connect)
+AC_SUBST(SASL_LIBS)
+AC_ARG_ENABLE(cyrus-sasl, AC_HELP_STRING([--enable-cyrus-sasl], [enable Cyrus SASL support for jabberd (no)]), want_cyrus=$enableval, want_cyrus=no)
+if test "x-$want_cyrus" = "x-yes" ; then
+  AC_CHECK_LIB(sasl2, sasl_client_init, [AC_DEFINE(HAVE_CYRUS_SASL, [1], [Define to 1 if Cyrus SASL is present]) SASL_LIBS=-"lsasl2"], [AC_ERROR(Cyrus SASL library not found)])
+fi
 
 dnl checks for zephyr
 AC_DEFINE(ZEPHYR_INT32, long, [Size of an int32.])
@@ -1261,6 +1266,8 @@
 echo Build with NAS support........ : $enable_nas
 echo Build with GtkSpell support... : $enable_gtkspell
 echo
+echo Use Cyrus SASL with jabber.... : $enable_cyrus_sasl
+echo
 echo Use kerberos 4 with zephyr.... : $kerberos
 echo Use external libzephyr........ : $zephyr
 echo
diff -ur --exclude aclocal.m4 --exclude config.h --exclude config.h.in --exclude configure --exclude Makefile.in gaim-1.3.1/src/protocols/jabber/auth.c gaim-1.3.1-cyrus/src/protocols/jabber/auth.c
--- gaim-1.3.1/src/protocols/jabber/auth.c	2005-01-09 19:16:11.000000000 +0000
+++ gaim-1.3.1-cyrus/src/protocols/jabber/auth.c	2005-07-01 20:33:23.816020192 +0100
@@ -115,13 +115,186 @@
 	gaim_connection_error(account->gc, _("Server requires plaintext authentication over an unencrypted stream"));
 }
 
+#ifdef HAVE_CYRUS_SASL
+
+static void jabber_auth_start_cyrus(JabberStream *);
+
+/* Callbacks for Cyrus SASL */
+
+static int jabber_sasl_cb_realm(void *ctx, int id, const char **avail, const char **result) 
+{
+	JabberStream *js = (JabberStream *)ctx;
+
+	if (id != SASL_CB_GETREALM || !result) return SASL_BADPARAM;
+
+	*result = js->user->domain;
+	
+	return SASL_OK;
+}
+
+static int jabber_sasl_cb_simple(void *ctx, int id, const char **res, unsigned *len)
+{
+	JabberStream *js = (JabberStream *)ctx;
+
+	switch(id) {
+	case SASL_CB_AUTHNAME:
+		*res = js->user->node;
+		break;
+	case SASL_CB_USER:
+		*res = js->user->node;
+		break;
+	default:
+		return SASL_BADPARAM;
+	}
+	if (len) *len = strlen((char *)*res);
+	return SASL_OK;
+}
+
+static int jabber_sasl_cb_secret(sasl_conn_t *conn, void *ctx, int id, sasl_secret_t **secret)
+{
+	JabberStream *js = (JabberStream *)ctx;
+	const char *pw = gaim_account_get_password(js->gc->account);
+	size_t len;
+	static sasl_secret_t *x = NULL;
+
+	if (!conn || !secret || id != SASL_CB_PASS)
+		return SASL_BADPARAM;
+
+	len = strlen(pw);
+	x = (sasl_secret_t *) realloc(x, sizeof(sasl_secret_t) + len);
+
+	if (!x)
+		return SASL_NOMEM;
+	x->len = len;
+	strcpy(x->data, pw);
+
+	*secret = x;
+	return SASL_OK;
+}
+
+static void allow_cyrus_plaintext_auth(GaimAccount *account)
+{
+	gaim_account_set_bool(account, "auth_plain_in_clear", TRUE);
+
+	jabber_auth_start_cyrus(account->gc->proto_data);
+}
+
+static void jabber_auth_start_cyrus(JabberStream *js)
+{
+	const char *clientout, *mech;
+	char *enc_out;
+	unsigned coutlen;
+	xmlnode *auth;
+	sasl_security_properties_t secprops;
+	gboolean again;
+	gboolean plaintext = TRUE;
+	
+	/* Set up security properties and options */
+	secprops.min_ssf = 0;
+	secprops.security_flags = SASL_SEC_NOANONYMOUS;
+
+	if (!js->gsc) {
+		plaintext = gaim_account_get_bool(js->gc->account, "auth_plain_in_clear", FALSE);
+		if (!plaintext)
+			secprops.security_flags |= SASL_SEC_NOPLAINTEXT;	
+		secprops.max_ssf = -1;
+		secprops.maxbufsize = 4096;
+	} else {
+		plaintext = FALSE;
+		secprops.max_ssf = 0;
+		secprops.maxbufsize = 0;
+	}
+	secprops.property_names = 0;
+	secprops.property_values = 0;
+
+	do { 	
+		again = FALSE;
+		/* Use the user's domain for compatibility with the old 
+		 * DIGESTMD5 code. Note that this may cause problems where 
+		 * the user's domain doesn't match the FQDN of the jabber 
+		 * service
+         	 */
+
+		js->sasl_state = sasl_client_new("xmpp", js->user->domain, NULL, NULL, js->sasl_cb, 0, &js->sasl);
+		if (js->sasl_state==SASL_OK) {
+			sasl_setprop(js->sasl, SASL_SEC_PROPS, &secprops);
+	        	js->sasl_state = sasl_client_start(js->sasl, js->sasl_mechs->str, NULL, &clientout, &coutlen, &mech);
+		}
+		switch (js->sasl_state) {
+		/* Success */
+		case SASL_CONTINUE:
+			break;
+		case SASL_NOMECH:
+		/* No mechanisms do what we want. See if we can add 
+		 * plaintext ones to the list. */
+
+			if (!gaim_account_get_password(js->gc->account)) {
+				gaim_connection_error(js->gc, _("Server couldn't authenticate you without a password"));
+				return;
+			} else if (!plaintext) {
+				gaim_request_yes_no(js->gc, _("Plaintext Authentication"),
+						_("Plaintext Authentication"),
+						_("This server requires plaintext authentication over an unencrypted connection.  Allow this and continue authentication?"),
+						2, js->gc->account, 
+						allow_cyrus_plaintext_auth,
+                                        	disallow_plaintext_auth);
+				return;
+			} else {
+				gaim_connection_error(js->gc, _("Server does not use any supported authentication method"));
+				return;
+			}
+			/* not reached */
+			break;
+
+		/* Fatal errors. Give up and go home */
+		case SASL_BADPARAM:
+		case SASL_NOMEM:
+			break;
+
+		/* For everything else, fail the mechanism and try again */
+		default:
+			if (strlen(mech)>0) {
+				char *pos;
+				pos = strstr(js->sasl_mechs->str,mech);
+				g_assert(pos!=NULL);
+				g_string_erase(js->sasl_mechs, pos-js->sasl_mechs->str,strlen(mech));
+			}
+			sasl_dispose(&js->sasl);
+			again=TRUE;
+		}
+	} while (again);
+
+	if (js->sasl_state == SASL_CONTINUE) {
+		auth = xmlnode_new("auth");
+		xmlnode_set_attrib(auth, "xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
+		xmlnode_set_attrib(auth,"mechanism", mech);
+		if (clientout) {
+	  		if (coutlen == 0) {
+	    			xmlnode_insert_data(auth, "=", -1);
+          		} else {
+				enc_out = gaim_base64_encode(clientout, coutlen);
+            			xmlnode_insert_data(auth, enc_out, -1);
+				g_free(enc_out);
+			}
+		}
+		jabber_send(js, auth);
+		xmlnode_free(auth);
+	} else {
+		gaim_connection_error(js->gc, "SASL authentication failed\n");
+	}
+}
+
+#endif
+
 void
 jabber_auth_start(JabberStream *js, xmlnode *packet)
 {
 	xmlnode *mechs, *mechnode;
-
+#ifdef HAVE_CYRUS_SASL
+	int id;
+#else
 	gboolean digest_md5 = FALSE, plain=FALSE;
-
+#endif
 
 	if(js->registration) {
 		jabber_register_start(js);
@@ -135,18 +308,58 @@
 		return;
 	}
 
+#ifdef HAVE_CYRUS_SASL
+	js->sasl_mechs = g_string_new("");
+#endif
 	for(mechnode = xmlnode_get_child(mechs, "mechanism"); mechnode;
 			mechnode = xmlnode_get_next_twin(mechnode))
 	{
 		char *mech_name = xmlnode_get_data(mechnode);
+#ifdef HAVE_CYRUS_SASL
+		g_string_append(js->sasl_mechs,mech_name);
+		g_string_append_c(js->sasl_mechs,' ');
+#else
 		if(mech_name && !strcmp(mech_name, "DIGEST-MD5"))
 			digest_md5 = TRUE;
 		else if(mech_name && !strcmp(mech_name, "PLAIN"))
 			plain = TRUE;
+#endif
 		g_free(mech_name);
 	}
+	
+#ifdef HAVE_CYRUS_SASL
+	js->auth_type = JABBER_AUTH_CYRUS;
+
+	/* Set up our callbacks structure */
+	js->sasl_cb = g_new0(sasl_callback_t,5);
+
+	id = 0;
+	js->sasl_cb[id].id = SASL_CB_GETREALM;
+	js->sasl_cb[id].proc = jabber_sasl_cb_realm;
+	js->sasl_cb[id].context = (void *)js;
+	id++;
+
+	js->sasl_cb[id].id = SASL_CB_AUTHNAME;
+	js->sasl_cb[id].proc = jabber_sasl_cb_simple;
+	js->sasl_cb[id].context = (void *)js;
+	id++;
+
+	js->sasl_cb[id].id = SASL_CB_USER;
+	js->sasl_cb[id].proc = jabber_sasl_cb_simple;
+	js->sasl_cb[id].context = (void *)js;
+	id++;
+
+	if (gaim_account_get_password(js->gc->account)) {
+		js->sasl_cb[id].id = SASL_CB_PASS;
+		js->sasl_cb[id].proc = jabber_sasl_cb_secret;
+		js->sasl_cb[id].context = (void *)js;
+		id++;
+	}
 
+	js->sasl_cb[id].id = SASL_CB_LIST_END;
 
+	jabber_auth_start_cyrus(js);
+#else
 	if(digest_md5) {
 		xmlnode *auth;
 
@@ -173,6 +386,7 @@
 		gaim_connection_error(js->gc,
 				_("Server does not use any supported authentication method"));
 	}
+#endif
 }
 
 static void auth_old_result_cb(JabberStream *js, xmlnode *packet, gpointer data)
@@ -457,17 +671,69 @@
 		g_free(dec_in);
 		g_hash_table_destroy(parts);
 	}
+#ifdef HAVE_CYRUS_SASL
+	else if (js->auth_type == JABBER_AUTH_CYRUS) {
+		char *enc_in = xmlnode_get_data(packet);
+		char *dec_in;
+		char *enc_out;
+		const char *c_out;
+		unsigned int clen,declen;
+		xmlnode *response;
+
+		gaim_base64_decode(enc_in, &dec_in, &declen);
+
+		js->sasl_state = sasl_client_step(js->sasl, dec_in, declen, 
+						  NULL, &c_out, &clen);
+		g_free(dec_in);
+		if (js->sasl_state != SASL_CONTINUE && js->sasl_state != SASL_OK) {
+			fprintf(stderr,"Error is %d : %s\n",js->sasl_state,sasl_errdetail(js->sasl));
+			gaim_connection_error(js->gc, _("SASL error"));
+			return;
+		} else {
+			response = xmlnode_new("response");
+			xmlnode_set_attrib(response, "xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
+			if (c_out>0) {
+				enc_out = gaim_base64_encode(c_out, clen);
+				xmlnode_insert_data(response, enc_out, -1);
+				g_free(enc_out);
+			}
+			jabber_send(js, response);
+			xmlnode_free(response);
+		}
+	}
+#endif
 }
 
 void jabber_auth_handle_success(JabberStream *js, xmlnode *packet)
 {
 	const char *ns = xmlnode_get_attrib(packet, "xmlns");
+#ifdef HAVE_CYRUS_SASL
+	int *x;
+#endif
 
 	if(!ns || strcmp(ns, "urn:ietf:params:xml:ns:xmpp-sasl")) {
 		gaim_connection_error(js->gc, _("Invalid response from server."));
 		return;
 	}
 
+#if HAVE_CYRUS_SASL
+	/* The SASL docs say that if the client hasn't returned OK yet, we
+	 * should try one more round against it
+	 */
+	if (js->sasl_state != SASL_OK) {
+		js->sasl_state = sasl_client_step(js->sasl, NULL, 0, NULL, NULL, NULL);
+		if (js->sasl_state != SASL_OK) {
+			/* This should never happen! */
+			gaim_connection_error(js->gc, _("Invalid response from server."));
+		}
+	}
+	/* If we've negotiated a security layer, we need to enable it */
+	sasl_getprop(js->sasl, SASL_SSF, (const void **)&x);
+	if (*x>0) {
+		sasl_getprop(js->sasl, SASL_MAXOUTBUF, (const void **)&x);
+		js->sasl_maxbuf = *x;
+	}
+#endif
 	jabber_stream_set_state(js, JABBER_STREAM_REINITIALIZING);
 }
 
diff -ur --exclude aclocal.m4 --exclude config.h --exclude config.h.in --exclude configure --exclude Makefile.in gaim-1.3.1/src/protocols/jabber/jabber.c gaim-1.3.1-cyrus/src/protocols/jabber/jabber.c
--- gaim-1.3.1/src/protocols/jabber/jabber.c	2005-06-09 05:12:22.000000000 +0100
+++ gaim-1.3.1-cyrus/src/protocols/jabber/jabber.c	2005-07-01 20:37:04.359492488 +0100
@@ -201,6 +201,42 @@
 		gaim_debug(GAIM_DEBUG_MISC, "jabber", "Sending%s: %s\n",
 				js->gsc ? " (ssl)" : "", data);
 
+	/* If we've got a security layer, we need to encode the data,
+	 * splitting it on the maximum buffer length negotiated */
+
+#ifdef HAVE_CYRUS_SASL
+	if (js->sasl_maxbuf>0) {
+		int pos;
+
+		if (!js->gsc && js->fd<0)
+			return;
+		pos = 0;
+		if (len == -1)
+			len = strlen(data);
+		while (pos < len) {
+			int towrite;
+			const char *out;
+			unsigned olen;
+
+			if ((len - pos) < js->sasl_maxbuf)
+				towrite = len - pos;
+			else
+				towrite = js->sasl_maxbuf;
+
+			sasl_encode(js->sasl, &data[pos], towrite, &out, &olen);
+			pos += towrite;
+
+			if (js->gsc)
+				ret = gaim_ssl_write(js->gsc, out, olen);
+			else
+				ret = write(js->fd, out, olen);
+			if (ret < 0)
+				gaim_connection_error(js->gc, _("Write error"));
+		}
+		return;
+	}
+#endif
+	
 	if(js->gsc) {
 		ret = gaim_ssl_write(js->gsc, data, len == -1 ? strlen(data) : len);
 	} else {
@@ -264,6 +300,18 @@
 		return;
 
 	if((len = read(js->fd, buf, sizeof(buf) - 1)) > 0) {
+#ifdef HAVE_CYRUS_SASL
+		if (js->sasl_maxbuf>0) {
+			const char *out;
+			int olen;
+			sasl_decode(js->sasl, buf, len, &out, &olen);
+			if (olen>0) {
+				gaim_debug(GAIM_DEBUG_INFO, "jabber", "RecvSASL (%d): %s\n", olen, out);
+				jabber_parser_process(js,out,olen);
+			}
+			return;
+		}
+#endif
 		buf[len] = '\0';
 		gaim_debug(GAIM_DEBUG_INFO, "jabber", "Recv (%d): %s\n", len, buf);
 		jabber_parser_process(js, buf, len);
@@ -785,6 +833,14 @@
 		g_free(js->stream_id);
 	if(js->user)
 		jabber_id_free(js->user);
+#ifdef HAVE_CYRUS_SASL
+	if(js->sasl)
+		sasl_dispose(&js->sasl);
+	if(js->sasl_mechs)
+		g_string_free(js->sasl_mechs,TRUE);
+	if(js->sasl_cb)
+		g_free(js->sasl_cb);
+#endif
 	g_free(js);
 }
 
@@ -1517,7 +1573,8 @@
 
 static GaimPluginProtocolInfo prpl_info =
 {
-	OPT_PROTO_CHAT_TOPIC | OPT_PROTO_UNIQUE_CHATNAME,
+	OPT_PROTO_CHAT_TOPIC | OPT_PROTO_UNIQUE_CHATNAME |
+	OPT_PROTO_PASSWORD_OPTIONAL,
 	NULL,							/* user_splits */
 	NULL,							/* protocol_options */
 	NO_BUDDY_ICONS,					/* icon_spec */
@@ -1649,6 +1706,10 @@
 	gaim_prefs_add_none("/plugins/prpl/jabber");
 	gaim_prefs_add_bool("/plugins/prpl/jabber/hide_os", FALSE);
 
+	/* XXX - If any other plugin wants SASL this won't be good ... */
+#ifdef HAVE_CYRUS_SASL
+	sasl_client_init(NULL);
+#endif
 	jabber_register_commands();
 }
 
diff -ur --exclude aclocal.m4 --exclude config.h --exclude config.h.in --exclude configure --exclude Makefile.in gaim-1.3.1/src/protocols/jabber/jabber.h gaim-1.3.1-cyrus/src/protocols/jabber/jabber.h
--- gaim-1.3.1/src/protocols/jabber/jabber.h	2004-03-05 23:42:34.000000000 +0000
+++ gaim-1.3.1-cyrus/src/protocols/jabber/jabber.h	2005-07-01 16:09:21.000000000 +0100
@@ -30,6 +30,10 @@
 #include "jutil.h"
 #include "xmlnode.h"
 
+#ifdef HAVE_CYRUS_SASL
+#include <sasl/sasl.h>
+#endif
+
 typedef enum {
 	JABBER_CAP_NONE           = 0,
 	JABBER_CAP_XHTML          = 1 << 0,
@@ -65,11 +69,19 @@
 		JABBER_AUTH_UNKNOWN,
 		JABBER_AUTH_DIGEST_MD5,
 		JABBER_AUTH_PLAIN,
-		JABBER_AUTH_IQ_AUTH
+		JABBER_AUTH_IQ_AUTH,
+		JABBER_AUTH_CYRUS
 	} auth_type;
 	char *stream_id;
 	JabberStreamState state;
 
+#ifdef HAVE_CYRUS_SASL
+	sasl_conn_t *sasl;
+	sasl_callback_t *sasl_cb;
+	int sasl_state;
+	int sasl_maxbuf;
+	GString *sasl_mechs;
+#endif
 	/* SASL authentication */
 	char *expected_rspauth;
 
diff -ur --exclude aclocal.m4 --exclude config.h --exclude config.h.in --exclude configure --exclude Makefile.in gaim-1.3.1/src/protocols/jabber/Makefile.am gaim-1.3.1-cyrus/src/protocols/jabber/Makefile.am
--- gaim-1.3.1/src/protocols/jabber/Makefile.am	2005-04-29 18:02:05.000000000 +0100
+++ gaim-1.3.1-cyrus/src/protocols/jabber/Makefile.am	2005-07-01 01:39:36.000000000 +0100
@@ -36,7 +36,7 @@
 
 AM_CFLAGS = $(st)
 
-libjabber_la_LDFLAGS = -module -avoid-version $(GLIB_LIBS)
+libjabber_la_LDFLAGS = -module -avoid-version $(GLIB_LIBS) $(SASL_LIBS)
 
 if STATIC_JABBER

---

From: Simon Wilkinson <[email-masked]>
Date: Sat, 06 Aug 2005 18:25:08 +0100

Hi again,

I should have said in my earlier email that, once applying the patch to
Gaim, you need to build with --enable-cyrus-sasl

Cheers,

Simon

---

From: Simon Wilkinson <[email-masked]>
Date: Wed, 19 Oct 2005 20:18:25 +0100

Hi Greg,

Just to let you know, I've now added support for kerberized Jabber
connections to AdiumX (on the Mac) and Gaim (for Windows). My new Gaim
patches remove the need for CyrusSASL - making it a lot easier to use
them on platforms that don't have it installed.

Let me know if you're interested in the patches.

Cheers.

Simon.

---

From: Greg Hudson <[email-masked]>
Date: Wed, 19 Oct 2005 15:22:14 -0400

Ah, excellent to hear there's been progress on your front.  We are just
getting close to being in a position to test your patches in our
environment, and are interested in getting your updated patches.

We decided to give up on Exodus for Windows and just use Gaim there.