-
Notifications
You must be signed in to change notification settings - Fork 376
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f2e29fa
commit 2fd7003
Showing
5 changed files
with
705 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
-- Prosody IM | ||
-- Copyright (C) 2019 Kim Alvefur | ||
-- | ||
-- This project is MIT/X11 licensed. Please see the | ||
-- COPYING file in the source package for more information. | ||
-- | ||
-- XEP-0388: Extensible SASL Profile | ||
-- | ||
|
||
local st = require "util.stanza"; | ||
local errors = require "util.error"; | ||
local base64 = require "util.encodings".base64; | ||
local jid_join = require "util.jid".join; | ||
local set = require "util.set"; | ||
|
||
local usermanager_get_sasl_handler = require "core.usermanager".get_sasl_handler; | ||
local sm_make_authenticated = require "core.sessionmanager".make_authenticated; | ||
|
||
local xmlns_sasl2 = "urn:xmpp:sasl:2"; | ||
|
||
local secure_auth_only = module:get_option_boolean("c2s_require_encryption", module:get_option_boolean("require_encryption", true)); | ||
local allow_unencrypted_plain_auth = module:get_option_boolean("allow_unencrypted_plain_auth", false) | ||
local insecure_mechanisms = module:get_option_set("insecure_sasl_mechanisms", allow_unencrypted_plain_auth and {} or {"PLAIN", "LOGIN"}); | ||
local disabled_mechanisms = module:get_option_set("disable_sasl_mechanisms", { "DIGEST-MD5" }); | ||
|
||
local host = module.host; | ||
|
||
local function tls_unique(self) | ||
return self.userdata["tls-unique"]:ssl_peerfinished(); | ||
end | ||
|
||
local function tls_exporter(conn) | ||
if not conn.ssl_exportkeyingmaterial then return end | ||
return conn:ssl_exportkeyingmaterial("EXPORTER-Channel-Binding", 32, ""); | ||
end | ||
|
||
local function sasl_tls_exporter(self) | ||
return tls_exporter(self.userdata["tls-exporter"]); | ||
end | ||
|
||
module:hook("stream-features", function(event) | ||
local origin, features = event.origin, event.features; | ||
local log = origin.log or module._log; | ||
|
||
if origin.type ~= "c2s_unauthed" then | ||
log("debug", "Already authenticated"); | ||
return | ||
elseif secure_auth_only and not origin.secure then | ||
log("debug", "Not offering authentication on insecure connection"); | ||
return; | ||
end | ||
|
||
local sasl_handler = usermanager_get_sasl_handler(host, origin) | ||
origin.sasl_handler = sasl_handler; | ||
|
||
local channel_bindings = set.new() | ||
if origin.encrypted then | ||
-- check whether LuaSec has the nifty binding to the function needed for tls-unique | ||
-- FIXME: would be nice to have this check only once and not for every socket | ||
if sasl_handler.add_cb_handler then | ||
local info = origin.conn:ssl_info(); | ||
if info and info.protocol == "TLSv1.3" then | ||
log("debug", "Channel binding 'tls-unique' undefined in context of TLS 1.3"); | ||
if tls_exporter(origin.conn) then | ||
log("debug", "Channel binding 'tls-exporter' supported"); | ||
sasl_handler:add_cb_handler("tls-exporter", sasl_tls_exporter); | ||
channel_bindings:add("tls-exporter"); | ||
else | ||
log("debug", "Channel binding 'tls-exporter' not supported"); | ||
end | ||
elseif origin.conn.ssl_peerfinished and origin.conn:ssl_peerfinished() then | ||
log("debug", "Channel binding 'tls-unique' supported"); | ||
sasl_handler:add_cb_handler("tls-unique", tls_unique); | ||
channel_bindings:add("tls-unique"); | ||
else | ||
log("debug", "Channel binding 'tls-unique' not supported (by LuaSec?)"); | ||
end | ||
sasl_handler["userdata"] = { | ||
["tls-unique"] = origin.conn; | ||
["tls-exporter"] = origin.conn; | ||
}; | ||
else | ||
log("debug", "Channel binding not supported by SASL handler"); | ||
end | ||
end | ||
|
||
local mechanisms = st.stanza("authentication", { xmlns = xmlns_sasl2 }); | ||
|
||
local available_mechanisms = sasl_handler:mechanisms() | ||
for mechanism in pairs(available_mechanisms) do | ||
if disabled_mechanisms:contains(mechanism) then | ||
log("debug", "Not offering disabled mechanism %s", mechanism); | ||
elseif not origin.secure and insecure_mechanisms:contains(mechanism) then | ||
log("debug", "Not offering mechanism %s on insecure connection", mechanism); | ||
else | ||
log("debug", "Offering mechanism %s", mechanism); | ||
mechanisms:text_tag("mechanism", mechanism); | ||
end | ||
end | ||
|
||
features:add_direct_child(mechanisms); | ||
|
||
local inline = st.stanza("inline"); | ||
module:fire_event("advertise-sasl-features", { origin = origin, features = inline, stream = event.stream }); | ||
mechanisms:add_direct_child(inline); | ||
end, 1); | ||
|
||
local function handle_status(session, status, ret, err_msg) | ||
local err = nil; | ||
if status == "error" then | ||
ret, err = nil, ret; | ||
if not errors.is_err(err) then | ||
err = errors.new({ condition = err, text = err_msg }, { session = session }); | ||
end | ||
end | ||
|
||
return module:fire_event("sasl2/"..session.base_type.."/"..status, { | ||
session = session, | ||
message = ret; | ||
error = err; | ||
error_text = err_msg; | ||
}); | ||
end | ||
|
||
module:hook("sasl2/c2s/failure", function (event) | ||
module:fire_event("authentication-failure", event); | ||
local session, condition, text = event.session, event.message, event.error_text; | ||
local failure = st.stanza("failure", { xmlns = xmlns_sasl2 }) | ||
:tag(condition, { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl" }):up(); | ||
if text then | ||
failure:text_tag("text", text); | ||
end | ||
session.send(failure); | ||
return true; | ||
end); | ||
|
||
module:hook("sasl2/c2s/error", function (event) | ||
local session = event.session | ||
session.send(st.stanza("failure", { xmlns = xmlns_sasl2 }) | ||
:tag(event.error and event.error.condition)); | ||
return true; | ||
end); | ||
|
||
module:hook("sasl2/c2s/challenge", function (event) | ||
local session = event.session; | ||
session.send(st.stanza("challenge", { xmlns = xmlns_sasl2 }) | ||
:text(base64.encode(event.message))); | ||
return true; | ||
end); | ||
|
||
module:hook("sasl2/c2s/success", function (event) | ||
local session = event.session | ||
local ok, err = sm_make_authenticated(session, session.sasl_handler.username); | ||
if not ok then | ||
handle_status(session, "failure", err); | ||
return true; | ||
end | ||
event.success = st.stanza("success", { xmlns = xmlns_sasl2 }); | ||
if event.message then | ||
event.success:text_tag("additional-data", base64.encode(event.message)); | ||
end | ||
end, 1000); | ||
|
||
module:hook("sasl2/c2s/success", function (event) | ||
local session = event.session | ||
event.success:text_tag("authorization-identifier", jid_join(session.username, session.host, session.resource)); | ||
session.send(event.success); | ||
end, -1000); | ||
|
||
module:hook("sasl2/c2s/success", function (event) | ||
module:fire_event("authentication-success", event); | ||
local session = event.session; | ||
local features = st.stanza("stream:features"); | ||
module:fire_event("stream-features", { origin = session, features = features }); | ||
session.send(features); | ||
end, -1500); | ||
|
||
-- The gap here is to allow modules to do stuff to the stream after the stanza | ||
-- is sent, but before we proceed with anything else. This is expected to be | ||
-- a common pattern with SASL2, which allows atomic negotiation of a bunch of | ||
-- stream features. | ||
module:hook("sasl2/c2s/success", function (event) --luacheck: ignore 212/event | ||
event.session.sasl_handler = nil; | ||
return true; | ||
end, -2000); | ||
|
||
local function process_cdata(session, cdata) | ||
if cdata then | ||
cdata = base64.decode(cdata); | ||
if not cdata then | ||
return handle_status(session, "failure", "incorrect-encoding"); | ||
end | ||
end | ||
return handle_status(session, session.sasl_handler:process(cdata)); | ||
end | ||
|
||
module:hook_tag(xmlns_sasl2, "authenticate", function (session, auth) | ||
if secure_auth_only and not session.secure then | ||
return handle_status(session, "failure", "encryption-required"); | ||
end | ||
local sasl_handler = session.sasl_handler; | ||
if not sasl_handler then | ||
sasl_handler = usermanager_get_sasl_handler(host, session); | ||
session.sasl_handler = sasl_handler; | ||
end | ||
local mechanism = assert(auth.attr.mechanism); | ||
if not sasl_handler:select(mechanism) then | ||
return handle_status(session, "failure", "invalid-mechanism"); | ||
end | ||
local user_agent = auth:get_child("user-agent"); | ||
if user_agent then | ||
session.client_id = user_agent.attr.id; | ||
sasl_handler.user_agent = { | ||
software = user_agent:get_child_text("software"); | ||
device = user_agent:get_child_text("device"); | ||
}; | ||
end | ||
local initial = auth:get_child_text("initial-response"); | ||
return process_cdata(session, initial); | ||
end); | ||
|
||
module:hook_tag(xmlns_sasl2, "response", function (session, response) | ||
local sasl_handler = session.sasl_handler; | ||
if not sasl_handler or not sasl_handler.selected then | ||
return handle_status(session, "failure", "invalid-mechanism"); | ||
end | ||
return process_cdata(session, response:get_text()); | ||
end); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
local base64 = require "util.encodings".base64; | ||
local id = require "util.id"; | ||
local sha1 = require "util.hashes".sha1; | ||
local st = require "util.stanza"; | ||
|
||
local sm_bind_resource = require "core.sessionmanager".bind_resource; | ||
|
||
local xmlns_bind2 = "urn:xmpp:bind:0"; | ||
local xmlns_sasl2 = "urn:xmpp:sasl:2"; | ||
|
||
module:depends("sasl2"); | ||
|
||
-- Advertise what we can do | ||
|
||
module:hook("advertise-sasl-features", function(event) | ||
local bind = st.stanza("bind", { xmlns = xmlns_bind2 }); | ||
local inline = st.stanza("inline"); | ||
module:fire_event("advertise-bind-features", { origin = event.origin, features = inline }); | ||
bind:add_direct_child(inline); | ||
|
||
event.features:add_direct_child(bind); | ||
end, 1); | ||
|
||
-- Helper to actually bind a resource to a session | ||
|
||
local function do_bind(session, bind_request) | ||
local resource = session.sasl_handler.resource; | ||
|
||
if not resource then | ||
local client_name_tag = bind_request:get_child_text("tag"); | ||
if client_name_tag then | ||
local client_id = session.client_id; | ||
local tag_suffix = client_id and base64.encode(sha1(client_id):sub(1, 9)) or id.medium(); | ||
resource = ("%s~%s"):format(client_name_tag, tag_suffix); | ||
end | ||
end | ||
|
||
local success, err_type, err, err_msg = sm_bind_resource(session, resource); | ||
if not success then | ||
session.log("debug", "Resource bind failed: %s", err_msg or err); | ||
return nil, { type = err_type, condition = err, text = err_msg }; | ||
end | ||
|
||
session.log("debug", "Resource bound: %s", session.full_jid); | ||
return st.stanza("bound", { xmlns = xmlns_bind2 }); | ||
end | ||
|
||
-- Enable inline features requested by the client | ||
|
||
local function enable_features(session, bind_request, bind_result) | ||
module:fire_event("enable-bind-features", { | ||
session = session; | ||
request = bind_request; | ||
result = bind_result; | ||
}); | ||
end | ||
|
||
-- SASL 2 integration | ||
|
||
module:hook_tag(xmlns_sasl2, "authenticate", function (session, auth) | ||
-- Cache action for future processing (after auth success) | ||
session.sasl2_bind_request = auth:child_with_ns(xmlns_bind2); | ||
end, 100); | ||
|
||
module:hook("sasl2/c2s/success", function (event) | ||
local session = event.session; | ||
|
||
local bind_request = session.sasl2_bind_request; | ||
if not bind_request then return; end -- No bind requested | ||
session.sasl2_bind_request = nil; | ||
|
||
local sm_success = session.sasl2_sm_success; | ||
if sm_success and sm_success.type == "resumed" then | ||
return; -- No need to bind a resource | ||
end | ||
|
||
local bind_result, err = do_bind(session, bind_request); | ||
if not bind_result then | ||
bind_result = st.stanza("failed", { xmlns = xmlns_bind2 }) | ||
:add_error(err); | ||
else | ||
enable_features(session, bind_request, bind_result); | ||
end | ||
|
||
event.success:add_child(bind_result); | ||
end, 100); | ||
|
||
-- Inline features | ||
|
||
module:hook("advertise-bind-features", function (event) | ||
local features = event.features; | ||
features:tag("feature", { var = "urn:xmpp:carbons:2" }):up(); | ||
features:tag("feature", { var = "urn:xmpp:csi:0" }):up(); | ||
end); | ||
|
||
module:hook("enable-bind-features", function (event) | ||
local session, request = event.session, event.request; | ||
|
||
-- Carbons | ||
if request:get_child("enable", "urn:xmpp:carbons:2") then | ||
session.want_carbons = true; | ||
event.result:tag("enabled", { xmlns = "urn:xmpp:carbons:2" }):up(); | ||
end | ||
|
||
-- CSI | ||
local csi_state_tag = request:child_with_ns("urn:xmpp:csi:0"); | ||
if csi_state_tag then | ||
session.state = csi_state_tag.name; | ||
end | ||
end, 10); |
Oops, something went wrong.