1-- Prosody IM 2-- Copyright (C) 2008-2010 Matthew Wild 3-- Copyright (C) 2008-2010 Waqas Hussain 4-- 5-- This project is MIT/X11 licensed. Please see the 6-- COPYING file in the source package for more information. 7-- 8 9local softreq = require"util.dependencies".softreq; 10local ssl = softreq"ssl"; 11if not ssl then 12 return { 13 create_context = function () 14 return nil, "LuaSec (required for encryption) was not found"; 15 end; 16 reload_ssl_config = function () end; 17 } 18end 19 20local configmanager = require "core.configmanager"; 21local log = require "util.logger".init("certmanager"); 22local ssl_context = ssl.context or softreq"ssl.context"; 23local ssl_x509 = ssl.x509 or softreq"ssl.x509"; 24local ssl_newcontext = ssl.newcontext; 25local new_config = require"util.sslconfig".new; 26local stat = require "lfs".attributes; 27 28local tonumber, tostring = tonumber, tostring; 29local pairs = pairs; 30local t_remove = table.remove; 31local type = type; 32local io_open = io.open; 33local select = select; 34 35local prosody = prosody; 36local resolve_path = require"util.paths".resolve_relative_path; 37local config_path = prosody.paths.config or "."; 38 39local function test_option(option) 40 return not not ssl_newcontext({mode="server",protocol="sslv23",options={ option }}); 41end 42 43local luasec_major, luasec_minor = ssl._VERSION:match("^(%d+)%.(%d+)"); 44local luasec_version = tonumber(luasec_major) * 100 + tonumber(luasec_minor); 45local luasec_has = ssl.config or softreq"ssl.config" or { 46 algorithms = { 47 ec = luasec_version >= 5; 48 }; 49 capabilities = { 50 curves_list = luasec_version >= 7; 51 }; 52 options = { 53 cipher_server_preference = test_option("cipher_server_preference"); 54 no_ticket = test_option("no_ticket"); 55 no_compression = test_option("no_compression"); 56 single_dh_use = test_option("single_dh_use"); 57 single_ecdh_use = test_option("single_ecdh_use"); 58 no_renegotiation = test_option("no_renegotiation"); 59 }; 60}; 61 62local _ENV = nil; 63-- luacheck: std none 64 65-- Global SSL options if not overridden per-host 66local global_ssl_config = configmanager.get("*", "ssl"); 67 68local global_certificates = configmanager.get("*", "certificates") or "certs"; 69 70local crt_try = { "", "/%s.crt", "/%s/fullchain.pem", "/%s.pem", }; 71local key_try = { "", "/%s.key", "/%s/privkey.pem", "/%s.pem", }; 72 73local function find_cert(user_certs, name) 74 local certs = resolve_path(config_path, user_certs or global_certificates); 75 log("debug", "Searching %s for a key and certificate for %s...", certs, name); 76 for i = 1, #crt_try do 77 local crt_path = certs .. crt_try[i]:format(name); 78 local key_path = certs .. key_try[i]:format(name); 79 80 if stat(crt_path, "mode") == "file" then 81 if crt_path == key_path then 82 if key_path:sub(-4) == ".crt" then 83 key_path = key_path:sub(1, -4) .. "key"; 84 elseif key_path:sub(-13) == "fullchain.pem" then 85 key_path = key_path:sub(1, -14) .. "privkey.pem"; 86 end 87 end 88 89 if stat(key_path, "mode") == "file" then 90 log("debug", "Selecting certificate %s with key %s for %s", crt_path, key_path, name); 91 return { certificate = crt_path, key = key_path }; 92 end 93 end 94 end 95 log("debug", "No certificate/key found for %s", name); 96end 97 98local function find_host_cert(host) 99 if not host then return nil; end 100 return find_cert(configmanager.get(host, "certificate"), host) or find_host_cert(host:match("%.(.+)$")); 101end 102 103local function find_service_cert(service, port) 104 local cert_config = configmanager.get("*", service.."_certificate"); 105 if type(cert_config) == "table" then 106 cert_config = cert_config[port] or cert_config.default; 107 end 108 return find_cert(cert_config, service); 109end 110 111-- Built-in defaults 112local core_defaults = { 113 capath = "/etc/ssl/certs"; 114 depth = 9; 115 protocol = "tlsv1+"; 116 verify = (ssl_x509 and { "peer", "client_once", }) or "none"; 117 options = { 118 cipher_server_preference = luasec_has.options.cipher_server_preference; 119 no_ticket = luasec_has.options.no_ticket; 120 no_compression = luasec_has.options.no_compression and configmanager.get("*", "ssl_compression") ~= true; 121 single_dh_use = luasec_has.options.single_dh_use; 122 single_ecdh_use = luasec_has.options.single_ecdh_use; 123 no_renegotiation = luasec_has.options.no_renegotiation; 124 }; 125 verifyext = { "lsec_continue", "lsec_ignore_purpose" }; 126 curve = luasec_has.algorithms.ec and not luasec_has.capabilities.curves_list and "secp384r1"; 127 curveslist = { 128 "X25519", 129 "P-384", 130 "P-256", 131 "P-521", 132 }; 133 ciphers = { -- Enabled ciphers in order of preference: 134 "HIGH+kEECDH", -- Ephemeral Elliptic curve Diffie-Hellman key exchange 135 "HIGH+kEDH", -- Ephemeral Diffie-Hellman key exchange, if a 'dhparam' file is set 136 "HIGH", -- Other "High strength" ciphers 137 -- Disabled cipher suites: 138 "!PSK", -- Pre-Shared Key - not used for XMPP 139 "!SRP", -- Secure Remote Password - not used for XMPP 140 "!3DES", -- 3DES - slow and of questionable security 141 "!aNULL", -- Ciphers that does not authenticate the connection 142 }; 143} 144 145if luasec_has.curves then 146 for i = #core_defaults.curveslist, 1, -1 do 147 if not luasec_has.curves[ core_defaults.curveslist[i] ] then 148 t_remove(core_defaults.curveslist, i); 149 end 150 end 151else 152 core_defaults.curveslist = nil; 153end 154 155local path_options = { -- These we pass through resolve_path() 156 key = true, certificate = true, cafile = true, capath = true, dhparam = true 157} 158 159if luasec_version < 5 and ssl_x509 then 160 -- COMPAT mw/luasec-hg 161 for i=1,#core_defaults.verifyext do -- Remove lsec_ prefix 162 core_defaults.verify[#core_defaults.verify+1] = core_defaults.verifyext[i]:sub(6); 163 end 164end 165 166local function create_context(host, mode, ...) 167 local cfg = new_config(); 168 cfg:apply(core_defaults); 169 local service_name, port = host:match("^(%S+) port (%d+)$"); 170 if service_name then 171 cfg:apply(find_service_cert(service_name, tonumber(port))); 172 else 173 cfg:apply(find_host_cert(host)); 174 end 175 cfg:apply({ 176 mode = mode, 177 -- We can't read the password interactively when daemonized 178 password = function() log("error", "Encrypted certificate for %s requires 'ssl' 'password' to be set in config", host); end; 179 }); 180 cfg:apply(global_ssl_config); 181 182 for i = select('#', ...), 1, -1 do 183 cfg:apply(select(i, ...)); 184 end 185 local user_ssl_config = cfg:final(); 186 187 if mode == "server" then 188 if not user_ssl_config.certificate then return nil, "No certificate present in SSL/TLS configuration for "..host; end 189 if not user_ssl_config.key then return nil, "No key present in SSL/TLS configuration for "..host; end 190 end 191 192 for option in pairs(path_options) do 193 if type(user_ssl_config[option]) == "string" then 194 user_ssl_config[option] = resolve_path(config_path, user_ssl_config[option]); 195 else 196 user_ssl_config[option] = nil; 197 end 198 end 199 200 -- LuaSec expects dhparam to be a callback that takes two arguments. 201 -- We ignore those because it is mostly used for having a separate 202 -- set of params for EXPORT ciphers, which we don't have by default. 203 if type(user_ssl_config.dhparam) == "string" then 204 local f, err = io_open(user_ssl_config.dhparam); 205 if not f then return nil, "Could not open DH parameters: "..err end 206 local dhparam = f:read("*a"); 207 f:close(); 208 user_ssl_config.dhparam = function() return dhparam; end 209 end 210 211 local ctx, err = ssl_newcontext(user_ssl_config); 212 213 -- COMPAT Older LuaSec ignores the cipher list from the config, so we have to take care 214 -- of it ourselves (W/A for #x) 215 if ctx and user_ssl_config.ciphers then 216 local success; 217 success, err = ssl_context.setcipher(ctx, user_ssl_config.ciphers); 218 if not success then ctx = nil; end 219 end 220 221 if not ctx then 222 err = err or "invalid ssl config" 223 local file = err:match("^error loading (.-) %("); 224 if file then 225 local typ; 226 if file == "private key" then 227 typ = file; 228 file = user_ssl_config.key or "your private key"; 229 elseif file == "certificate" then 230 typ = file; 231 file = user_ssl_config.certificate or "your certificate file"; 232 end 233 local reason = err:match("%((.+)%)$") or "some reason"; 234 if reason == "Permission denied" then 235 reason = "Check that the permissions allow Prosody to read this file."; 236 elseif reason == "No such file or directory" then 237 reason = "Check that the path is correct, and the file exists."; 238 elseif reason == "system lib" then 239 reason = "Previous error (see logs), or other system error."; 240 elseif reason == "no start line" then 241 reason = "Check that the file contains a "..(typ or file); 242 elseif reason == "(null)" or not reason then 243 reason = "Check that the file exists and the permissions are correct"; 244 else 245 reason = "Reason: "..tostring(reason):lower(); 246 end 247 log("error", "SSL/TLS: Failed to load '%s': %s (for %s)", file, reason, host); 248 else 249 log("error", "SSL/TLS: Error initialising for %s: %s", host, err); 250 end 251 end 252 return ctx, err, user_ssl_config; 253end 254 255local function reload_ssl_config() 256 global_ssl_config = configmanager.get("*", "ssl"); 257 global_certificates = configmanager.get("*", "certificates") or "certs"; 258 if luasec_has.options.no_compression then 259 core_defaults.options.no_compression = configmanager.get("*", "ssl_compression") ~= true; 260 end 261end 262 263prosody.events.add_handler("config-reloaded", reload_ssl_config); 264 265return { 266 create_context = create_context; 267 reload_ssl_config = reload_ssl_config; 268 find_cert = find_cert; 269}; 270