Mercurial > prosody-modules
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; |