1-- mod_s2s_auth_dane 2-- Copyright (C) 2013-2014 Kim Alvefur 3-- 4-- This file is MIT/X11 licensed. 5-- 6-- Implements DANE and Secure Delegation using DNS SRV as described in 7-- http://tools.ietf.org/html/draft-miller-xmpp-dnssec-prooftype 8-- 9-- Known issues: 10-- Could be done much cleaner if mod_s2s was using util.async 11-- 12-- TODO Things to test/handle: 13-- Negative or bogus answers 14-- No encryption offered 15-- Different hostname before and after STARTTLS - mod_s2s should complain 16-- Interaction with Dialback 17-- 18-- luacheck: ignore module 19 20module:set_global(); 21 22local have_async, async = pcall(require, "util.async"); 23local noop = function () end 24local unpack = table.unpack or _G.unpack; 25local type = type; 26local t_insert = table.insert; 27local set = require"util.set"; 28local dns_lookup = require"net.adns".lookup; 29local hashes = require"util.hashes"; 30local base64 = require"util.encodings".base64; 31local idna_to_ascii = require "util.encodings".idna.to_ascii; 32local idna_to_unicode = require"util.encodings".idna.to_unicode; 33local nameprep = require"util.encodings".stringprep.nameprep; 34local cert_verify_identity = require "util.x509".verify_identity; 35 36do 37 local net_dns = require"net.dns"; 38 if not net_dns.types or not net_dns.types[52] then 39 module:log("error", "No TLSA support available, DANE will not be supported"); 40 return 41 end 42end 43 44local pat = "%-%-%-%-%-BEGIN ([A-Z ]+)%-%-%-%-%-\r?\n".. 45"([0-9A-Za-z=+/\r\n]*)\r?\n%-%-%-%-%-END %1%-%-%-%-%-"; 46local function pem2der(pem) 47 local typ, data = pem:match(pat); 48 if typ and data then 49 return base64.decode(data), typ; 50 end 51end 52local use_map = { ["DANE-EE"] = 3; ["DANE-TA"] = 2; ["PKIX-EE"] = 1; ["PKIX-CA"] = 0 } 53 54local implemented_uses = set.new { "DANE-EE", "PKIX-EE" }; 55do 56 local cert_mt = debug.getregistry()["SSL:Certificate"]; 57 if cert_mt and cert_mt.__index.issued then 58 -- Need cert:issued() for these 59 implemented_uses:add("DANE-TA"); 60 implemented_uses:add("PKIX-CA"); 61 else 62 module:log("debug", "The cert:issued() method is unavailable, DANE-TA and PKIX-CA can't be enabled"); 63 end 64 if not cert_mt.__index.pubkey then 65 module:log("debug", "The cert:pubkey() method is unavailable, the SPKI usage can't be supported"); 66 end 67end 68local configured_uses = module:get_option_set("dane_uses", { "DANE-EE", "DANE-TA" }); 69local enabled_uses = set.intersection(implemented_uses, configured_uses) / function(use) return use_map[use] end; 70local unsupported = configured_uses - implemented_uses; 71if not unsupported:empty() then 72 module:log("warn", "Unable to support DANE uses %s", tostring(unsupported)); 73end 74 75-- Find applicable TLSA records 76-- Takes a s2sin/out and a callback 77local function dane_lookup(host_session, cb) 78 cb = cb or noop; 79 local log = host_session.log or module._log; 80 if host_session.dane ~= nil then return end -- Has already done a lookup 81 82 if host_session.direction == "incoming" then 83 if not host_session.from_host then 84 log("debug", "Session doesn't have a 'from' host set"); 85 return; 86 end 87 -- We don't know what hostname or port to use for Incoming connections 88 -- so we do a SRV lookup and then request TLSA records for each SRV 89 -- Most servers will probably use the same certificate on outgoing 90 -- and incoming connections, so this should work well 91 local name = host_session.from_host and idna_to_ascii(host_session.from_host); 92 if not name then 93 log("warn", "Could not convert '%s' to ASCII for DNS lookup", tostring(host_session.from_host)); 94 return; 95 end 96 log("debug", "Querying SRV records from _xmpp-server._tcp.%s.", name); 97 host_session.dane = dns_lookup(function (answer, err) 98 host_session.dane = false; -- Mark that we already did the lookup 99 100 if not answer then 101 log("debug", "Resolver error: %s", tostring(err)); 102 return cb(host_session); 103 end 104 105 if answer.bogus then 106 log("warn", "Results are bogus!"); 107 -- Bad sign, probably not a good idea to do any fallback here 108 host_session.dane = answer; 109 elseif not answer.secure then 110 log("debug", "Results are not secure"); 111 return cb(host_session); 112 end 113 114 local n = answer.n or #answer; 115 if n == 0 then 116 -- No SRV records, synthesize fallback host and port 117 -- this may behave oddly for connections in the other direction if 118 -- mod_s2s doesn't keep the answer around 119 answer[1] = { srv = { target = name, port = 5269 } }; 120 n = 1; 121 elseif n == 1 and answer[1].srv.target == '.' then 122 return cb(host_session); -- No service ... This shouldn't happen? 123 end 124 local srv_hosts = { answer = answer }; 125 host_session.srv_hosts = srv_hosts; 126 local dane; 127 for _, record in ipairs(answer) do 128 t_insert(srv_hosts, record.srv); 129 log("debug", "Querying TLSA record for %s:%d", record.srv.target, record.srv.port); 130 dns_lookup(function(dane_answer) 131 log("debug", "Got answer for %s:%d", record.srv.target, record.srv.port); 132 n = n - 1; 133 -- There are three kinds of answers 134 -- Insecure, Secure and Bogus 135 -- 136 -- We collect Secure answers for later use 137 -- 138 -- Insecure (legacy) answers are simply ignored 139 -- 140 -- If we get a Bogus (dnssec error) reply, keep the 141 -- status around. If there were only bogus replies, the 142 -- connection will be aborted. If there were at least 143 -- one non-Bogus reply, we proceed. If none of the 144 -- replies matched, we consider the connection insecure. 145 146 if (dane_answer.bogus or dane_answer.secure) and not dane then 147 -- The first answer we care about 148 -- For services with only one SRV record, this will be the only one 149 log("debug", "First secure (or bogus) TLSA") 150 dane = dane_answer; 151 elseif dane_answer.bogus then 152 log("debug", "Got additional bogus TLSA") 153 dane.bogus = dane_answer.bogus; 154 elseif dane_answer.secure then 155 log("debug", "Got additional secure TLSA") 156 for _, dane_record in ipairs(dane_answer) do 157 t_insert(dane, dane_record); 158 end 159 end 160 if n == 0 then 161 if dane then 162 host_session.dane = dane; 163 if #dane > 0 and dane.bogus then 164 -- Got at least one non-bogus reply, 165 -- This should trigger a failure if one of them did not match 166 log("warn", "Ignoring bogus replies"); 167 dane.bogus = nil; 168 end 169 if #dane == 0 and dane.bogus == nil then 170 -- Got no usable data 171 host_session.dane = false; 172 end 173 end 174 return cb(host_session); 175 end 176 end, ("_%d._tcp.%s."):format(record.srv.port, record.srv.target), "TLSA"); 177 end 178 end, "_xmpp-server._tcp."..name..".", "SRV"); 179 return true; 180 elseif host_session.direction == "outgoing" then 181 -- Prosody has already done SRV lookups for outgoing session, so check if those are secure 182 local srv_hosts = host_session.srv_hosts; 183 if not ( srv_hosts and srv_hosts.answer and srv_hosts.answer.secure ) then 184 return; -- No secure SRV records, fall back to non-DANE mode 185 -- Empty response were not kept by older mod_s2s/s2sout 186 end 187 -- Do TLSA lookup for currently selected SRV record 188 local srv_choice = srv_hosts[host_session.srv_choice or 0] or { target = idna_to_ascii(host_session.to_host), port = 5269 }; 189 log("debug", "Querying TLSA record for %s:%d", srv_choice.target, srv_choice.port); 190 host_session.dane = dns_lookup(function(answer) 191 if answer and ((answer.secure and #answer > 0) or answer.bogus) then 192 srv_choice.dane = answer; 193 else 194 srv_choice.dane = false; 195 end 196 host_session.dane = srv_choice.dane; 197 return cb(host_session); 198 end, ("_%d._tcp.%s."):format(srv_choice.port, srv_choice.target), "TLSA"); 199 return true; 200 end 201end 202 203local function pause(host_session) 204 host_session.log("debug", "Pausing connection until DANE lookup is completed"); 205 host_session.conn:pause() 206end 207 208local function resume(host_session) 209 host_session.log("debug", "DANE lookup completed, resuming connection"); 210 host_session.conn:resume() 211end 212 213if have_async then 214 function pause(host_session) 215 host_session.log("debug", "Pausing connection until DANE lookup is completed"); 216 local wait, done = async.waiter(); 217 host_session._done_waiting_for_dane = done; 218 wait(); 219 end 220 local function _resume(_, host_session) 221 if host_session._done_waiting_for_dane then 222 host_session.log("debug", "DANE lookup completed, resuming connection"); 223 host_session._done_waiting_for_dane(); 224 host_session._done_waiting_for_dane = nil; 225 end 226 end 227 function resume(host_session) 228 -- Something about the way luaunbound calls callbacks is messed up 229 if host_session._done_waiting_for_dane then 230 module:add_timer(0, _resume, host_session); 231 end 232 end 233end 234 235local new_dane = module:get_option_boolean("use_dane", false); 236 237function module.add_host(module) 238 local function on_new_s2s(event) 239 local host_session = event.origin; 240 if host_session.type == "s2sout" or host_session.type == "s2sin" then 241 return; -- Already authenticated 242 end 243 if host_session.dane ~= nil then 244 return; -- Already done DANE lookup 245 end 246 dane_lookup(host_session, resume); 247 -- Let it run in parallel until we need to check the cert 248 end 249 250 if not new_dane then 251 -- New outgoing connections 252 module:hook("stanza/http://etherx.jabber.org/streams:features", on_new_s2s, 501); 253 module:hook("s2sout-authenticate-legacy", on_new_s2s, 200); 254 end 255 256 -- New incoming connections 257 module:hook("s2s-stream-features", on_new_s2s, 10); 258 259 module:hook("s2s-authenticated", function(event) 260 local session = event.session; 261 if session.dane and type(session.dane) == "table" and next(session.dane) ~= nil and not session.secure then 262 -- TLSA record but no TLS, not ok. 263 -- TODO Optional? 264 -- Bogus replies should trigger this path 265 -- How does this interact with Dialback? 266 session:close({ 267 condition = "policy-violation", 268 text = "Encrypted server-to-server communication is required but was not " 269 ..((session.direction == "outgoing" and "offered") or "used") 270 }); 271 return false; 272 end 273 -- Cleanup 274 session.srv_hosts = nil; 275 end); 276end 277 278-- Compare one TLSA record against a certificate 279local function one_dane_check(tlsa, cert, log) 280 local select, match, certdata = tlsa.select, tlsa.match; 281 282 if select == 0 then 283 certdata = pem2der(cert:pem()); 284 elseif select == 1 and cert.pubkey then 285 certdata = pem2der(cert:pubkey()); 286 else 287 log("warn", "DANE selector %s is unsupported", tlsa:getSelector() or select); 288 return; 289 end 290 291 if match == 1 then 292 certdata = hashes.sha256(certdata); 293 elseif match == 2 then 294 certdata = hashes.sha512(certdata); 295 elseif match ~= 0 then 296 log("warn", "DANE match rule %s is unsupported", tlsa:getMatchType() or match); 297 return; 298 end 299 300 if #certdata ~= #tlsa.data then 301 log("warn", "Length mismatch: Cert: %d, TLSA: %d", #certdata, #tlsa.data); 302 end 303 return certdata == tlsa.data; 304end 305 306module:hook("s2s-check-certificate", function(event) 307 local session, cert, host = event.session, event.cert, event.host; 308 if not cert then return end 309 local log = session.log or module._log; 310 local dane = session.dane; 311 if type(dane) ~= "table" then 312 if dane == nil and dane_lookup(session, resume) then 313 pause(session); 314 dane = session.dane; 315 end 316 end 317 if type(dane) == "table" then 318 local match_found, supported_found; 319 for i = 1, #dane do 320 local tlsa = dane[i].tlsa; 321 log("debug", "TLSA #%d: %s", i, tostring(tlsa)) 322 local use = tlsa.use; 323 324 if enabled_uses:contains(use) then 325 -- DANE-EE or PKIX-EE 326 if use == 3 or use == 1 then 327 -- Should we check if the cert subject matches? 328 local is_match = one_dane_check(tlsa, cert, log); 329 if is_match ~= nil then 330 supported_found = true; 331 end 332 if is_match and use == 1 and session.cert_chain_status ~= "valid" then 333 -- for usage 1, PKIX-EE, the chain has to be valid already 334 log("debug", "PKIX-EE TLSA matches untrusted certificate"); 335 is_match = false; 336 end 337 if is_match then 338 log("info", "DANE validated ok for %s using %s", host, tlsa:getUsage()); 339 session.cert_identity_status = "valid"; 340 if use == 3 then -- DANE-EE, chain status equals DNSSEC chain status 341 session.cert_chain_status = "valid"; 342 end 343 match_found = true; 344 dane.matching = tlsa; 345 break; 346 end 347 -- DANE-TA or PKIX-CA 348 elseif use == 2 or use == 0 then 349 supported_found = true; 350 local chain = session.conn:socket():getpeerchain(); 351 for c = 1, #chain do 352 local cacert = chain[c]; 353 local is_match = one_dane_check(tlsa, cacert, log); 354 if is_match ~= nil then 355 supported_found = true; 356 end 357 if is_match and not cacert:issued(cert, unpack(chain)) then 358 is_match = false; 359 end 360 if is_match and use == 0 and session.cert_chain_status ~= "valid" then 361 -- for usage 0, PKIX-CA, identity and chain has to be valid already 362 is_match = false; 363 end 364 if is_match then 365 log("info", "DANE validated ok for %s using %s", host, tlsa:getUsage()); 366 if use == 2 then -- DANE-TA 367 session.cert_identity_status = "valid"; 368 if cert_verify_identity(host, "xmpp-server", cert) then 369 session.cert_chain_status = "valid"; 370 -- else -- TODO Check against SRV target? 371 end 372 end 373 match_found = true; 374 dane.matching = tlsa; 375 break; 376 end 377 end 378 if match_found then break end 379 end 380 end 381 end 382 if supported_found and not match_found or dane.bogus then 383 -- No TLSA matched or response was bogus 384 local why = "No TLSA matched certificate"; 385 if dane.bogus then 386 why = "Bogus: "..tostring(dane.bogus); 387 end 388 log("warn", "DANE validation failed for %s: %s", host, why); 389 session.cert_identity_status = "invalid"; 390 session.cert_chain_status = "invalid"; 391 end 392 else 393 if session.cert_chain_status == "valid" and session.cert_identity_status ~= "valid" 394 and session.srv_hosts and session.srv_hosts.answer and session.srv_hosts.answer.secure then 395 local srv_hosts, srv_choice, srv_target = session.srv_hosts, session.srv_choice; 396 for i = srv_choice or 1, srv_choice or #srv_hosts do 397 srv_target = session.srv_hosts[i].target:gsub("%.?$",""); 398 log("debug", "Comparing certificate with Secure SRV target %s", srv_target); 399 srv_target = nameprep(idna_to_unicode(srv_target)); 400 if srv_target and cert_verify_identity(srv_target, "xmpp-server", cert) then 401 log("info", "Certificate for %s matches Secure SRV target %s", host, srv_target); 402 session.cert_identity_status = "valid"; 403 return; 404 end 405 end 406 end 407 end 408end); 409 410-- Telnet command 411if module:get_option_set("modules_enabled", {}):contains("admin_telnet") then 412 module:depends("admin_telnet"); -- Make sure the env is there 413 local def_env = module:shared("admin_telnet/env"); 414 415 local function annotate(session, line) 416 line = line or {}; 417 table.insert(line, "--"); 418 if session.dane == nil then 419 table.insert(line, "No DANE attempted, probably insecure SRV response"); 420 elseif session.dane == false then 421 table.insert(line, "DANE failed or response was insecure"); 422 elseif type(session.dane) ~= "table" then 423 table.insert(line, "Waiting for DANE records..."); 424 elseif session.dane.matching then 425 table.insert(line, "Matching DANE record:\n| " .. tostring(session.dane.matching)); 426 else 427 table.insert(line, "DANE records:\n| " .. tostring(session.dane)); 428 end 429 return table.concat(line, " "); 430 end 431 432 function def_env.s2s:show_dane(...) 433 return self:show(..., annotate); 434 end 435end 436 437