comparison mod_s2s_auth_dane/mod_s2s_auth_dane.lua @ 1626:aed20f9e78c8

mod_s2s_auth_dane: Comments and cleanup
author Kim Alvefur <zash@zash.se>
date Mon, 16 Mar 2015 16:19:53 +0100
parents 6ea13869753f
children a4a6b4be973a
comparison
equal deleted inserted replaced
1625:c427de617ada 1626: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;