Diff

plugins/mod_saslauth.lua @ 3651:337391d34b70

s2s: SASL EXTERNAL
author Paul Aurich <paul@darkrain42.org>
date Sun, 21 Nov 2010 21:10:43 -0800
parent 3553:1f0af8572f15
child 3733:26571a99f6e6
line wrap: on
line diff
--- a/plugins/mod_saslauth.lua	Sun Nov 21 21:02:31 2010 -0800
+++ b/plugins/mod_saslauth.lua	Sun Nov 21 21:10:43 2010 -0800
@@ -11,8 +11,11 @@
 local st = require "util.stanza";
 local sm_bind_resource = require "core.sessionmanager".bind_resource;
 local sm_make_authenticated = require "core.sessionmanager".make_authenticated;
+local s2s_make_authenticated = require "core.s2smanager".make_authenticated;
 local base64 = require "util.encodings".base64;
 
+local cert_verify_identity = require "util.certverification".verify_identity;
+
 local nodeprep = require "util.encodings".stringprep.nodeprep;
 local usermanager_get_sasl_handler = require "core.usermanager".get_sasl_handler;
 local t_concat, t_insert = table.concat, table.insert;
@@ -91,8 +94,123 @@
 	return true;
 end
 
+module:hook_stanza(xmlns_sasl, "success", function (session, stanza)
+	if session.type ~= "s2sout_unauthed" or session.external_auth ~= "attempting" then return; end
+	module:log("debug", "SASL EXTERNAL with %s succeeded", session.to_host);
+	session.external_auth = "succeeded"
+	session:reset_stream();
+
+	local default_stream_attr = {xmlns = "jabber:server", ["xmlns:stream"] = "http://etherx.jabber.org/streams",
+	                            ["xmlns:db"] = 'jabber:server:dialback', version = "1.0", to = session.to_host, from = session.from_host};
+	session.sends2s("<?xml version='1.0'?>");
+	session.sends2s(st.stanza("stream:stream", default_stream_attr):top_tag());
+
+	s2s_make_authenticated(session, session.to_host);
+	return true;
+end)
+
+module:hook_stanza(xmlns_sasl, "failure", function (session, stanza)
+	if session.type ~= "s2sout_unauthed" or session.external_auth ~= "attempting" then return; end
+
+	module:log("info", "SASL EXTERNAL with %s failed", session.to_host)
+	-- TODO: Log the failure reason
+	session.external_auth = "failed"
+end, 500)
+
+module:hook_stanza(xmlns_sasl, "failure", function (session, stanza)
+	-- TODO: Dialback wasn't loaded.  Do something useful.
+end, 90)
+
+module:hook_stanza("http://etherx.jabber.org/streams", "features", function (session, stanza)
+	if session.type ~= "s2sout_unauthed" or not session.secure then return; end
+
+	local mechanisms = stanza:get_child("mechanisms", xmlns_sasl)
+	if mechanisms then
+		for mech in mechanisms:childtags() do
+			if mech[1] == "EXTERNAL" then
+				module:log("debug", "Initiating SASL EXTERNAL with %s", session.to_host);
+				local reply = st.stanza("auth", {xmlns = xmlns_sasl, mechanism = "EXTERNAL"});
+				reply:text(base64.encode(session.from_host))
+				session.sends2s(reply)
+				session.external_auth = "attempting"
+				return true
+			end
+		end
+	end
+end, 150);
+
+local function s2s_external_auth(session, stanza)
+	local mechanism = stanza.attr.mechanism;
+
+	if not session.secure then
+		if mechanism == "EXTERNAL" then
+			session.sends2s(build_reply("failure", "encryption-required"))
+		else
+			session.sends2s(build_reply("failure", "invalid-mechanism"))
+		end
+		return true;
+	end
+
+	if mechanism ~= "EXTERNAL" or session.cert_chain_status ~= "valid" then
+		session.sends2s(build_reply("failure", "invalid-mechanism"))
+		return true;
+	end
+
+	local text = stanza[1]
+	if not text then
+		session.sends2s(build_reply("failure", "malformed-request"))
+		return true
+	end
+
+	-- Either the value is "=" and we've already verified the external
+	-- cert identity, or the value is a string and either matches the
+	-- from_host (
+
+	text = base64.decode(text)
+	if not text then
+		session.sends2s(build_reply("failure", "incorrect-encoding"))
+		return true;
+	end
+
+	if session.cert_identity_status == "valid" then
+		if text ~= "" and text ~= session.from_host then
+			session.sends2s(build_reply("failure", "invalid-authzid"))
+			return true
+		end
+	else
+		if text == "" then
+			session.sends2s(build_reply("failure", "invalid-authzid"))
+			return true
+		end
+
+		local cert = session.conn:socket():getpeercertificate()
+		if (cert_verify_identity(text, "xmpp-server", cert)) then
+			session.cert_identity_status = "valid"
+		else
+			session.cert_identity_status = "invalid"
+			session.sends2s(build_reply("failure", "invalid-authzid"))
+			return true
+		end
+	end
+
+	session.external_auth = "succeeded"
+
+	if not session.from_host then
+		session.from_host = text;
+	end
+	session.sends2s(build_reply("success"))
+	module:log("info", "Accepting SASL EXTERNAL identity from %s", text or session.from_host);
+	s2s_make_authenticated(session, text or session.from_host)
+	session:reset_stream();
+	return true
+end
+
 module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event)
 	local session, stanza = event.origin, event.stanza;
+	if session.type == "s2sin_unauthed" then
+		return s2s_external_auth(session, stanza)
+	end
+
 	if session.type ~= "c2s_unauthed" then return; end
 
 	if session.sasl_handler and session.sasl_handler.selected then
@@ -168,6 +286,20 @@
 	end
 end);
 
+module:hook("s2s-stream-features", function(event)
+	local origin, features = event.origin, event.features;
+	if origin.secure and origin.type == "s2sin_unauthed" then
+		-- Offer EXTERNAL if chain is valid and either we didn't validate
+		-- the identity or it passed.
+		if origin.cert_chain_status == "valid" and origin.cert_identity_status ~= "invalid" then --TODO: Configurable
+			module:log("debug", "Offering SASL EXTERNAL")
+			features:tag("mechanisms", { xmlns = xmlns_sasl })
+				:tag("mechanism"):text("EXTERNAL")
+			:up():up();
+		end
+	end
+end);
+
 module:hook("iq/self/urn:ietf:params:xml:ns:xmpp-bind:bind", function(event)
 	local origin, stanza = event.origin, event.stanza;
 	local resource;