aboutsummaryrefslogtreecommitdiffstats
path: root/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'plugins')
-rw-r--r--plugins/mod_admin_telnet.lua108
-rw-r--r--plugins/mod_dialback.lua14
-rw-r--r--plugins/mod_saslauth.lua132
3 files changed, 250 insertions, 4 deletions
diff --git a/plugins/mod_admin_telnet.lua b/plugins/mod_admin_telnet.lua
index 712e9eb7..da40f57e 100644
--- a/plugins/mod_admin_telnet.lua
+++ b/plugins/mod_admin_telnet.lua
@@ -19,6 +19,7 @@ local console_listener = { default_port = 5582; default_mode = "*l"; default_int
require "util.iterators";
local jid_bare = require "util.jid".bare;
local set, array = require "util.set", require "util.array";
+local cert_verify_identity = require "util.x509".verify_identity;
local commands = {};
local def_env = {};
@@ -498,7 +499,7 @@ function def_env.s2s:show(match_jid)
for remotehost, session in pairs(host_session.s2sout) do
if (not match_jid) or remotehost:match(match_jid) or host:match(match_jid) then
count_out = count_out + 1;
- print(" "..host.." -> "..remotehost..(session.secure and " (encrypted)" or "")..(session.compressed and " (compressed)" or ""));
+ print(" "..host.." -> "..remotehost..(session.cert_identity_status == "valid" and " (secure)" or "")..(session.secure and " (encrypted)" or "")..(session.compressed and " (compressed)" or ""));
if session.sendq then
print(" There are "..#session.sendq.." queued outgoing stanzas for this connection");
end
@@ -535,7 +536,7 @@ function def_env.s2s:show(match_jid)
-- Pft! is what I say to list comprehensions
or (session.hosts and #array.collect(keys(session.hosts)):filter(subhost_filter)>0)) then
count_in = count_in + 1;
- print(" "..host.." <- "..(session.from_host or "(unknown)")..(session.secure and " (encrypted)" or "")..(session.compressed and " (compressed)" or ""));
+ print(" "..host.." <- "..(session.from_host or "(unknown)")..(session.cert_identity_status == "valid" and " (secure)" or "")..(session.secure and " (encrypted)" or "")..(session.compressed and " (compressed)" or ""));
if session.type == "s2sin_unauthed" then
print(" Connection not yet authenticated");
end
@@ -561,6 +562,109 @@ function def_env.s2s:show(match_jid)
return true, "Total: "..count_out.." outgoing, "..count_in.." incoming connections";
end
+local function print_subject(print, subject)
+ for _, entry in ipairs(subject) do
+ print(
+ (" %s: %q"):format(
+ entry.name or entry.oid,
+ entry.value:gsub("[\r\n%z%c]", " ")
+ )
+ );
+ end
+end
+
+function def_env.s2s:showcert(domain)
+ local ser = require "util.serialization".serialize;
+ local print = self.session.print;
+ local domain_sessions = set.new(array.collect(keys(incoming_s2s)))
+ /function(session) return session.from_host == domain; end;
+ for local_host in values(prosody.hosts) do
+ local s2sout = local_host.s2sout;
+ if s2sout and s2sout[domain] then
+ domain_sessions:add(s2sout[domain]);
+ end
+ end
+ local cert_set = {};
+ for session in domain_sessions do
+ local conn = session.conn;
+ conn = conn and conn:socket();
+ if not conn.getpeercertificate then
+ if conn.dohandshake then
+ error("This version of LuaSec does not support certificate viewing");
+ end
+ else
+ local cert = conn:getpeercertificate();
+ if cert then
+ local digest = cert:digest("sha1");
+ if not cert_set[digest] then
+ local chain_valid, chain_err = conn:getpeerchainvalid();
+ cert_set[digest] = {
+ {
+ from = session.from_host,
+ to = session.to_host,
+ direction = session.direction
+ };
+ chain_valid = chain_valid;
+ chain_err = chain_err;
+ cert = cert;
+ };
+ else
+ table.insert(cert_set[digest], {
+ from = session.from_host,
+ to = session.to_host,
+ direction = session.direction
+ });
+ end
+ end
+ end
+ end
+ local domain_certs = array.collect(values(cert_set));
+ -- Phew. We now have a array of unique certificates presented by domain.
+ local print = self.session.print;
+ local n_certs = #domain_certs;
+
+ if n_certs == 0 then
+ return "No certificates found for "..domain;
+ end
+
+ local function _capitalize_and_colon(byte)
+ return string.upper(byte)..":";
+ end
+ local function pretty_fingerprint(hash)
+ return hash:gsub("..", _capitalize_and_colon):sub(1, -2);
+ end
+
+ for cert_info in values(domain_certs) do
+ local cert = cert_info.cert;
+ print("---")
+ print("Fingerprint (SHA1): "..pretty_fingerprint(cert:digest("sha1")));
+ print("");
+ local n_streams = #cert_info;
+ print("Currently used on "..n_streams.." stream"..(n_streams==1 and "" or "s")..":");
+ for _, stream in ipairs(cert_info) do
+ if stream.direction == "incoming" then
+ print(" "..stream.to.." <- "..stream.from);
+ else
+ print(" "..stream.from.." -> "..stream.to);
+ end
+ end
+ print("");
+ local chain_valid, err = cert_info.chain_valid, cert_info.chain_err;
+ local valid_identity = cert_verify_identity(domain, "xmpp-server", cert);
+ print("Trusted certificate: "..(chain_valid and "Yes" or ("No ("..err..")")));
+ print("Issuer: ");
+ print_subject(print, cert:issuer());
+ print("");
+ print("Valid for "..domain..": "..(valid_identity and "Yes" or "No"));
+ print("Subject:");
+ print_subject(print, cert:subject());
+ end
+ print("---");
+ return ("Showing "..n_certs.." certificate"
+ ..(n_certs==1 and "" or "s")
+ .." presented by "..domain..".");
+end
+
function def_env.s2s:close(from, to)
local print, count = self.session.print, 0;
diff --git a/plugins/mod_dialback.lua b/plugins/mod_dialback.lua
index e1fd5a42..a8923e27 100644
--- a/plugins/mod_dialback.lua
+++ b/plugins/mod_dialback.lua
@@ -132,9 +132,19 @@ module:hook("stanza/jabber:server:dialback:result", function(event)
end
end);
+module:hook_stanza("urn:ietf:params:xml:ns:xmpp-sasl", "failure", function (origin, stanza)
+ if origin.external_auth == "failed" then
+ module:log("debug", "SASL EXTERNAL failed, falling back to dialback");
+ s2s_initiate_dialback(origin);
+ return true;
+ end
+end, 100);
+
module:hook_stanza(xmlns_stream, "features", function (origin, stanza)
- s2s_initiate_dialback(origin);
- return true;
+ if not origin.external_auth or origin.external_auth == "failed" then
+ s2s_initiate_dialback(origin);
+ return true;
+ end
end, 100);
-- Offer dialback to incoming hosts
diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua
index c6ead31a..7f9a27ad 100644
--- a/plugins/mod_saslauth.lua
+++ b/plugins/mod_saslauth.lua
@@ -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.x509".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 @@ local function sasl_process_cdata(session, stanza)
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 @@ module:hook("stream-features", function(event)
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;