2 -- Copyright (C) 2008-2010 Matthew Wild
3 -- Copyright (C) 2008-2010 Waqas Hussain
5 -- This project is MIT/X11 licensed. Please see the
6 -- COPYING file in the source package for more information.
9 local softreq
= require
"util.dependencies".softreq
;
10 local ssl
= softreq
"ssl";
13 create_context
= function ()
14 return nil, "LuaSec (required for encryption) was not found";
16 reload_ssl_config
= function () end;
20 local configmanager
= require
"core.configmanager";
21 local log = require
"util.logger".init("certmanager");
22 local ssl_context
= ssl
.context
or softreq
"ssl.context";
23 local ssl_newcontext
= ssl
.newcontext
;
24 local new_config
= require
"util.sslconfig".new
;
25 local stat
= require
"lfs".attributes
;
27 local tonumber, tostring = tonumber, tostring;
29 local t_remove
= table.remove;
31 local io_open
= io
.open
;
32 local select
= select
;
34 local prosody
= prosody
;
35 local resolve_path
= require
"util.paths".resolve_relative_path
;
36 local config_path
= prosody
.paths
.config
or ".";
38 local luasec_major
, luasec_minor
= ssl
._VERSION
:match("^(%d+)%.(%d+)");
39 local luasec_version
= tonumber(luasec_major
) * 100 + tonumber(luasec_minor
);
40 local luasec_has
= softreq
"ssl.config" or {
42 ec
= luasec_version
>= 5;
45 curves_list
= luasec_version
>= 7;
48 cipher_server_preference
= luasec_version
>= 2;
49 no_ticket
= luasec_version
>= 4;
50 no_compression
= luasec_version
>= 5;
51 single_dh_use
= luasec_version
>= 2;
52 single_ecdh_use
= luasec_version
>= 2;
59 -- Global SSL options if not overridden per-host
60 local global_ssl_config
= configmanager
.get("*", "ssl");
62 local global_certificates
= configmanager
.get("*", "certificates") or "certs";
64 local crt_try
= { "", "/%s.crt", "/%s/fullchain.pem", "/%s.pem", };
65 local key_try
= { "", "/%s.key", "/%s/privkey.pem", "/%s.pem", };
67 local function find_cert(user_certs
, name
)
68 local certs
= resolve_path(config_path
, user_certs
or global_certificates
);
69 log("debug", "Searching %s for a key and certificate for %s...", certs
, name
);
70 for i
= 1, #crt_try
do
71 local crt_path
= certs
.. crt_try
[i
]:format(name
);
72 local key_path
= certs
.. key_try
[i
]:format(name
);
74 if stat(crt_path
, "mode") == "file" then
75 if key_path
:sub(-4) == ".crt" then
76 key_path
= key_path
:sub(1, -4) .. "key";
77 if stat(key_path
, "mode") == "file" then
78 log("debug", "Selecting certificate %s with key %s for %s", crt_path
, key_path
, name
);
79 return { certificate
= crt_path
, key
= key_path
};
81 elseif stat(key_path
, "mode") == "file" then
82 log("debug", "Selecting certificate %s with key %s for %s", crt_path
, key_path
, name
);
83 return { certificate
= crt_path
, key
= key_path
};
87 log("debug", "No certificate/key found for %s", name
);
90 local function find_host_cert(host
)
91 if not host
then return nil; end
92 return find_cert(configmanager
.get(host
, "certificate"), host
) or find_host_cert(host
:match("%.(.+)$"));
95 local function find_service_cert(service
, port
)
96 local cert_config
= configmanager
.get("*", service
.."_certificate");
97 if type(cert_config
) == "table" then
98 cert_config
= cert_config
[port
] or cert_config
.default
;
100 return find_cert(cert_config
, service
);
104 local core_defaults
= {
105 capath
= "/etc/ssl/certs";
110 cipher_server_preference
= luasec_has
.options
.cipher_server_preference
;
111 no_ticket
= luasec_has
.options
.no_ticket
;
112 no_compression
= luasec_has
.options
.no_compression
and configmanager
.get("*", "ssl_compression") ~= true;
113 single_dh_use
= luasec_has
.options
.single_dh_use
;
114 single_ecdh_use
= luasec_has
.options
.single_ecdh_use
;
116 verifyext
= { "lsec_continue", "lsec_ignore_purpose" };
117 curve
= luasec_has
.algorithms
.ec
and not luasec_has
.capabilities
.curves_list
and "secp384r1";
124 ciphers
= { -- Enabled ciphers in order of preference:
125 "HIGH+kEECDH", -- Ephemeral Elliptic curve Diffie-Hellman key exchange
126 "HIGH+kEDH", -- Ephemeral Diffie-Hellman key exchange, if a 'dhparam' file is set
127 "HIGH", -- Other "High strength" ciphers
128 -- Disabled cipher suites:
129 "!PSK", -- Pre-Shared Key - not used for XMPP
130 "!SRP", -- Secure Remote Password - not used for XMPP
131 "!3DES", -- 3DES - slow and of questionable security
132 "!aNULL", -- Ciphers that does not authenticate the connection
136 if luasec_has
.curves
then
137 for i
= #core_defaults
.curveslist
, 1, -1 do
138 if not luasec_has
.curves
[ core_defaults
.curveslist
[i
] ] then
139 t_remove(core_defaults
.curveslist
, i
);
143 core_defaults
.curveslist
= nil;
146 local path_options
= { -- These we pass through resolve_path()
147 key
= true, certificate
= true, cafile
= true, capath
= true, dhparam
= true
150 local function create_context(host
, mode
, ...)
151 local cfg
= new_config();
152 cfg
:apply(core_defaults
);
153 local service_name
, port
= host
:match("^(%S+) port (%d+)$");
155 cfg
:apply(find_service_cert(service_name
, tonumber(port
)));
157 cfg
:apply(find_host_cert(host
));
161 -- We can't read the password interactively when daemonized
162 password
= function() log("error", "Encrypted certificate for %s requires 'ssl' 'password' to be set in config", host
); end;
164 cfg
:apply(global_ssl_config
);
166 for i
= select('#', ...), 1, -1 do
167 cfg
:apply(select(i
, ...));
169 local user_ssl_config
= cfg
:final();
171 if mode
== "server" then
172 if not user_ssl_config
.certificate
then
173 log("info", "No certificate present in SSL/TLS configuration for %s. SNI will be required.", host
);
175 if user_ssl_config
.certificate
and not user_ssl_config
.key
then return nil, "No key present in SSL/TLS configuration for "..host
; end
178 for option
in pairs(path_options
) do
179 if type(user_ssl_config
[option
]) == "string" then
180 user_ssl_config
[option
] = resolve_path(config_path
, user_ssl_config
[option
]);
182 user_ssl_config
[option
] = nil;
186 -- LuaSec expects dhparam to be a callback that takes two arguments.
187 -- We ignore those because it is mostly used for having a separate
188 -- set of params for EXPORT ciphers, which we don't have by default.
189 if type(user_ssl_config
.dhparam
) == "string" then
190 local f
, err
= io_open(user_ssl_config
.dhparam
);
191 if not f
then return nil, "Could not open DH parameters: "..err
end
192 local dhparam
= f
:read("*a");
194 user_ssl_config
.dhparam
= function() return dhparam
; end
197 local ctx
, err
= ssl_newcontext(user_ssl_config
);
199 -- COMPAT Older LuaSec ignores the cipher list from the config, so we have to take care
200 -- of it ourselves (W/A for #x)
201 if ctx
and user_ssl_config
.ciphers
then
203 success
, err
= ssl_context
.setcipher(ctx
, user_ssl_config
.ciphers
);
204 if not success
then ctx
= nil; end
208 err
= err
or "invalid ssl config"
209 local file
= err
:match("^error loading (.-) %(");
212 if file
== "private key" then
214 file
= user_ssl_config
.key
or "your private key";
215 elseif file
== "certificate" then
217 file
= user_ssl_config
.certificate
or "your certificate file";
219 local reason
= err
:match("%((.+)%)$") or "some reason";
220 if reason
== "Permission denied" then
221 reason
= "Check that the permissions allow Prosody to read this file.";
222 elseif reason
== "No such file or directory" then
223 reason
= "Check that the path is correct, and the file exists.";
224 elseif reason
== "system lib" then
225 reason
= "Previous error (see logs), or other system error.";
226 elseif reason
== "no start line" then
227 reason
= "Check that the file contains a "..(typ
or file
);
228 elseif reason
== "(null)" or not reason
then
229 reason
= "Check that the file exists and the permissions are correct";
231 reason
= "Reason: "..tostring(reason
):lower();
233 log("error", "SSL/TLS: Failed to load '%s': %s (for %s)", file
, reason
, host
);
235 log("error", "SSL/TLS: Error initialising for %s: %s", host
, err
);
238 return ctx
, err
, user_ssl_config
;
241 local function reload_ssl_config()
242 global_ssl_config
= configmanager
.get("*", "ssl");
243 global_certificates
= configmanager
.get("*", "certificates") or "certs";
244 if luasec_has
.options
.no_compression
then
245 core_defaults
.options
.no_compression
= configmanager
.get("*", "ssl_compression") ~= true;
249 prosody
.events
.add_handler("config-reloaded", reload_ssl_config
);
252 create_context
= create_context
;
253 reload_ssl_config
= reload_ssl_config
;
254 find_cert
= find_cert
;