mod_s2s_auth_dane/mod_s2s_auth_dane.lua
changeset 1630 aed20f9e78c8
parent 1507 6ea13869753f
child 1646 a4a6b4be973a
equal deleted inserted replaced
1629:c427de617ada 1630:aed20f9e78c8
    15 -- Different hostname before and after STARTTLS - mod_s2s should complain
    15 -- Different hostname before and after STARTTLS - mod_s2s should complain
    16 -- Interaction with Dialback
    16 -- Interaction with Dialback
    17 
    17 
    18 module:set_global();
    18 module:set_global();
    19 
    19 
       
    20 local noop = function () end
    20 local type = type;
    21 local type = type;
    21 local t_insert = table.insert;
    22 local t_insert = table.insert;
    22 local set = require"util.set";
    23 local set = require"util.set";
    23 local dns_lookup = require"net.adns".lookup;
    24 local dns_lookup = require"net.adns".lookup;
    24 local hashes = require"util.hashes";
    25 local hashes = require"util.hashes";
    58 	end
    59 	end
    59 end
    60 end
    60 local configured_uses = module:get_option_set("dane_uses", { "DANE-EE", "DANE-TA" });
    61 local configured_uses = module:get_option_set("dane_uses", { "DANE-EE", "DANE-TA" });
    61 local enabled_uses = set.intersection(implemented_uses, configured_uses) / function(use) return use_map[use] end;
    62 local enabled_uses = set.intersection(implemented_uses, configured_uses) / function(use) return use_map[use] end;
    62 
    63 
    63 local function dane_lookup(host_session, cb, a,b,c,e)
    64 -- Find applicable TLSA records
    64 	if host_session.dane ~= nil then return end
    65 -- Takes a s2sin/out and a callback
       
    66 local function dane_lookup(host_session, cb)
       
    67 	cb = cb or noop;
       
    68 	if host_session.dane ~= nil then return end -- Has already done a lookup
       
    69 
    65 	if host_session.direction == "incoming" then
    70 	if host_session.direction == "incoming" then
       
    71 		-- We don't know what hostname or port to use for Incoming connections
       
    72 		-- so we do a SRV lookup and then request TLSA records for each SRV
       
    73 		-- Most servers will probably use the same certificate on outgoing
       
    74 		-- and incoming connections, so this should work well
    66 		local name = host_session.from_host and idna_to_ascii(host_session.from_host);
    75 		local name = host_session.from_host and idna_to_ascii(host_session.from_host);
    67 		if not name then return end
    76 		if not name then
    68 		host_session.dane = dns_lookup(function (answer)
    77 			module:log("error", "Could not convert '%s' to ASCII for DNS lookup", tostring(host_session.from_host));
    69 			host_session.dane = false;
    78 				return;
       
    79 			end
       
    80 		host_session.dane = dns_lookup(function (answer, err)
       
    81 			host_session.dane = false; -- Mark that we already did the lookup
       
    82 
       
    83 			if not answer then
       
    84 				module:log("debug", "Resolver error: %s", tostring(err));
       
    85 				return cb(host_session);
       
    86 			end
       
    87 
    70 			if not answer.secure then
    88 			if not answer.secure then
    71 				if cb then return cb(a,b,c,e); end
    89 				module:log("debug", "Results are not secure");
    72 				return;
    90 				return cb(host_session);
    73 			end
    91 			end
       
    92 
    74 			local n = #answer
    93 			local n = #answer
    75 			if n == 0 then if cb then return cb(a,b,c,e); end return end
    94 			if n == 0 then
    76 			if n == 1 and answer[1].srv.target == '.' then return end
    95 				-- No SRV records, we could proceed with the domainname and
       
    96 				-- default port but that will currently not work properly since
       
    97 				-- mod_s2s doesn't keep the answer around for that
       
    98 				return cb(host_session);
       
    99 			end
       
   100 			if n == 1 and answer[1].srv.target == '.' then
       
   101 				return cb(host_session); -- No service ... This shouldn't happen?
       
   102 			end
    77 			local srv_hosts = { answer = answer };
   103 			local srv_hosts = { answer = answer };
    78 			local dane = {};
   104 			local dane = {};
    79 			host_session.dane = dane;
   105 			host_session.dane = dane;
    80 			host_session.srv_hosts = srv_hosts;
   106 			host_session.srv_hosts = srv_hosts;
    81 			for _, record in ipairs(answer) do
   107 			for _, record in ipairs(answer) do
    82 				t_insert(srv_hosts, record.srv);
   108 				t_insert(srv_hosts, record.srv);
    83 				dns_lookup(function(dane_answer)
   109 				dns_lookup(function(dane_answer)
    84 					n = n - 1;
   110 					n = n - 1;
    85 					if dane_answer.bogus then
   111 					if dane_answer.bogus then
    86 						-- How to handle this?
   112 						dane.bogus = dane_answer.bogus;
    87 					elseif dane_answer.secure then
   113 					elseif dane_answer.secure then
    88 						for _, record in ipairs(dane_answer) do
   114 						for _, record in ipairs(dane_answer) do
    89 							t_insert(dane, record);
   115 							t_insert(dane, record);
    90 						end
   116 						end
    91 					end
   117 					end
    92 					if n == 0 and cb then return cb(a,b,c,e); end
   118 					if n == 0 then
       
   119 						if #dane > 0 and dane.bogus then
       
   120 							-- Got at least one non-bogus reply,
       
   121 							-- This should trigger a failure if one of them did not match
       
   122 							host_session.log("warn", "Ignoring bogus replies");
       
   123 							dane.bogus = nil;
       
   124 						end
       
   125 						if #dane == 0 and dane.bogus == nil then
       
   126 							-- Got no usable data
       
   127 							host_session.dane = false;
       
   128 						end
       
   129 						return cb(host_session);
       
   130 					end
    93 				end, ("_%d._tcp.%s."):format(record.srv.port, record.srv.target), "TLSA");
   131 				end, ("_%d._tcp.%s."):format(record.srv.port, record.srv.target), "TLSA");
    94 			end
   132 			end
    95 		end, "_xmpp-server._tcp."..name..".", "SRV");
   133 		end, "_xmpp-server._tcp."..name..".", "SRV");
    96 		return true;
   134 		return true;
    97 	elseif host_session.direction == "outgoing" then
   135 	elseif host_session.direction == "outgoing" then
       
   136 		-- Prosody has already done SRV lookups for outgoing session, so check if those are secure
    98 		local srv_hosts = host_session.srv_hosts;
   137 		local srv_hosts = host_session.srv_hosts;
    99 		if not ( srv_hosts and srv_hosts.answer and srv_hosts.answer.secure )  then return end
   138 		if not ( srv_hosts and srv_hosts.answer and srv_hosts.answer.secure ) then
       
   139 			return; -- No secure SRV records, fall back to non-DANE mode
       
   140 		end
       
   141 		-- Do TLSA lookup for currently selected SRV record
   100 		local srv_choice = srv_hosts[host_session.srv_choice];
   142 		local srv_choice = srv_hosts[host_session.srv_choice];
   101 		host_session.dane = dns_lookup(function(answer)
   143 		host_session.dane = dns_lookup(function(answer)
   102 			if answer and ((answer.secure and #answer > 0) or answer.bogus) then
   144 			if answer and ((answer.secure and #answer > 0) or answer.bogus) then
   103 				srv_choice.dane = answer;
   145 				srv_choice.dane = answer;
   104 			else
   146 			else
   105 				srv_choice.dane = false;
   147 				srv_choice.dane = false;
   106 			end
   148 			end
   107 			host_session.dane = srv_choice.dane;
   149 			host_session.dane = srv_choice.dane;
   108 			if cb then return cb(a,b,c,e); end
   150 			return cb(host_session);
   109 		end, ("_%d._tcp.%s."):format(srv_choice.port, srv_choice.target), "TLSA");
   151 		end, ("_%d._tcp.%s."):format(srv_choice.port, srv_choice.target), "TLSA");
   110 		return true;
   152 		return true;
   111 	end
   153 	end
       
   154 end
       
   155 
       
   156 local function resume(host_session)
       
   157 	host_session.log("debug", "DANE lookup completed, resuming connection");
       
   158 	host_session.conn:resume()
   112 end
   159 end
   113 
   160 
   114 function module.add_host(module)
   161 function module.add_host(module)
   115 	local function on_new_s2s(event)
   162 	local function on_new_s2s(event)
   116 		local host_session = event.origin;
   163 		local host_session = event.origin;
   117 		if host_session.type == "s2sout" or host_session.type == "s2sin" or host_session.dane ~= nil then return end -- Already authenticated
   164 		if host_session.type == "s2sout" or host_session.type == "s2sin" then
   118 		local function resume()
   165 			return; -- Already authenticated
   119 			host_session.log("debug", "DANE lookup completed, resuming connection");
   166 		end
   120 			host_session.conn:resume()
   167 		if host_session.dane ~= nil then
       
   168 			return; -- Already done DANE lookup
   121 		end
   169 		end
   122 		if dane_lookup(host_session, resume) then
   170 		if dane_lookup(host_session, resume) then
   123 			host_session.log("debug", "Pausing connection until DANE lookup is completed");
   171 			host_session.log("debug", "Pausing connection until DANE lookup is completed");
   124 			host_session.conn:pause()
   172 			host_session.conn:pause()
   125 		end
   173 		end
   132 	-- New incoming connections
   180 	-- New incoming connections
   133 	module:hook("s2s-stream-features", on_new_s2s, 10);
   181 	module:hook("s2s-stream-features", on_new_s2s, 10);
   134 
   182 
   135 	module:hook("s2s-authenticated", function(event)
   183 	module:hook("s2s-authenticated", function(event)
   136 		local session = event.session;
   184 		local session = event.session;
   137 		if session.dane and not session.secure then
   185 		if session.dane and next(session.dane) ~= nil and not session.secure then
   138 			-- TLSA record but no TLS, not ok.
   186 			-- TLSA record but no TLS, not ok.
   139 			-- TODO Optional?
   187 			-- TODO Optional?
   140 			-- Bogus replies should trigger this path
   188 			-- Bogus replies should trigger this path
   141 			-- How does this interact with Dialback?
   189 			-- How does this interact with Dialback?
   142 			session:close({
   190 			session:close({
   150 		session.dane = nil;
   198 		session.dane = nil;
   151 		session.srv_hosts = nil;
   199 		session.srv_hosts = nil;
   152 	end);
   200 	end);
   153 end
   201 end
   154 
   202 
       
   203 -- Compare one TLSA record against a certificate
   155 local function one_dane_check(tlsa, cert)
   204 local function one_dane_check(tlsa, cert)
   156 	local select, match, certdata = tlsa.select, tlsa.match;
   205 	local select, match, certdata = tlsa.select, tlsa.match;
   157 
   206 
   158 	if select == 0 then
   207 	if select == 0 then
   159 		certdata = pem2der(cert:pem());
   208 		certdata = pem2der(cert:pem());
   171 	elseif match ~= 0 then
   220 	elseif match ~= 0 then
   172 		module:log("warn", "DANE match rule %s is unsupported", tlsa:getMatchType() or match);
   221 		module:log("warn", "DANE match rule %s is unsupported", tlsa:getMatchType() or match);
   173 		return;
   222 		return;
   174 	end
   223 	end
   175 
   224 
       
   225 	if #certdata ~= #tlsa.data then
       
   226 		module:log("warn", "Length mismatch: Cert: %d, TLSA: %d", #certdata, #tlsa.data);
       
   227 	end
   176 	return certdata == tlsa.data;
   228 	return certdata == tlsa.data;
   177 end
   229 end
   178 
   230 
   179 module:hook("s2s-check-certificate", function(event)
   231 module:hook("s2s-check-certificate", function(event)
   180 	local session, cert, host = event.session, event.cert, event.host;
   232 	local session, cert, host = event.session, event.cert, event.host;