Software /
code /
prosody
Comparison
util/prosodyctl/cert.lua @ 10871:e5dee71d0ebb
prosodyctl+util.prosodyctl.*: Start breaking up the ever-growing prosodyctl
author | Matthew Wild <mwild1@gmail.com> |
---|---|
date | Tue, 02 Jun 2020 08:01:21 +0100 |
child | 11203:d10f59ac7f74 |
comparison
equal
deleted
inserted
replaced
10870:3f1889608f3e | 10871:e5dee71d0ebb |
---|---|
1 local lfs = require "lfs"; | |
2 | |
3 local pctl = require "util.prosodyctl"; | |
4 local configmanager = require "core.configmanager"; | |
5 | |
6 local openssl; | |
7 | |
8 local cert_commands = {}; | |
9 | |
10 -- If a file already exists, ask if the user wants to use it or replace it | |
11 -- Backups the old file if replaced | |
12 local function use_existing(filename) | |
13 local attrs = lfs.attributes(filename); | |
14 if attrs then | |
15 if pctl.show_yesno(filename .. " exists, do you want to replace it? [y/n]") then | |
16 local backup = filename..".bkp~"..os.date("%FT%T", attrs.change); | |
17 os.rename(filename, backup); | |
18 pctl.show_message("%s backed up to %s", filename, backup); | |
19 else | |
20 -- Use the existing file | |
21 return true; | |
22 end | |
23 end | |
24 end | |
25 | |
26 local have_pposix, pposix = pcall(require, "util.pposix"); | |
27 local cert_basedir = prosody.paths.data == "." and "./certs" or prosody.paths.data; | |
28 if have_pposix and pposix.getuid() == 0 then | |
29 -- FIXME should be enough to check if this directory is writable | |
30 local cert_dir = configmanager.get("*", "certificates") or "certs"; | |
31 cert_basedir = configmanager.resolve_relative_path(prosody.paths.config, cert_dir); | |
32 end | |
33 | |
34 function cert_commands.config(arg) | |
35 if #arg >= 1 and arg[1] ~= "--help" then | |
36 local conf_filename = cert_basedir .. "/" .. arg[1] .. ".cnf"; | |
37 if use_existing(conf_filename) then | |
38 return nil, conf_filename; | |
39 end | |
40 local distinguished_name; | |
41 if arg[#arg]:find("^/") then | |
42 distinguished_name = table.remove(arg); | |
43 end | |
44 local conf = openssl.config.new(); | |
45 conf:from_prosody(prosody.hosts, configmanager, arg); | |
46 if distinguished_name then | |
47 local dn = {}; | |
48 for k, v in distinguished_name:gmatch("/([^=/]+)=([^/]+)") do | |
49 table.insert(dn, k); | |
50 dn[k] = v; | |
51 end | |
52 conf.distinguished_name = dn; | |
53 else | |
54 pctl.show_message("Please provide details to include in the certificate config file."); | |
55 pctl.show_message("Leave the field empty to use the default value or '.' to exclude the field.") | |
56 for _, k in ipairs(openssl._DN_order) do | |
57 local v = conf.distinguished_name[k]; | |
58 if v then | |
59 local nv = nil; | |
60 if k == "commonName" then | |
61 v = arg[1] | |
62 elseif k == "emailAddress" then | |
63 v = "xmpp@" .. arg[1]; | |
64 elseif k == "countryName" then | |
65 local tld = arg[1]:match"%.([a-z]+)$"; | |
66 if tld and #tld == 2 and tld ~= "uk" then | |
67 v = tld:upper(); | |
68 end | |
69 end | |
70 nv = pctl.show_prompt(("%s (%s):"):format(k, nv or v)); | |
71 nv = (not nv or nv == "") and v or nv; | |
72 if nv:find"[\192-\252][\128-\191]+" then | |
73 conf.req.string_mask = "utf8only" | |
74 end | |
75 conf.distinguished_name[k] = nv ~= "." and nv or nil; | |
76 end | |
77 end | |
78 end | |
79 local conf_file, err = io.open(conf_filename, "w"); | |
80 if not conf_file then | |
81 pctl.show_warning("Could not open OpenSSL config file for writing"); | |
82 pctl.show_warning(err); | |
83 os.exit(1); | |
84 end | |
85 conf_file:write(conf:serialize()); | |
86 conf_file:close(); | |
87 print(""); | |
88 pctl.show_message("Config written to %s", conf_filename); | |
89 return nil, conf_filename; | |
90 else | |
91 pctl.show_usage("cert config HOSTNAME [HOSTNAME+]", "Builds a certificate config file covering the supplied hostname(s)") | |
92 end | |
93 end | |
94 | |
95 function cert_commands.key(arg) | |
96 if #arg >= 1 and arg[1] ~= "--help" then | |
97 local key_filename = cert_basedir .. "/" .. arg[1] .. ".key"; | |
98 if use_existing(key_filename) then | |
99 return nil, key_filename; | |
100 end | |
101 os.remove(key_filename); -- This file, if it exists is unlikely to have write permissions | |
102 local key_size = tonumber(arg[2] or pctl.show_prompt("Choose key size (2048):") or 2048); | |
103 local old_umask = pposix.umask("0377"); | |
104 if openssl.genrsa{out=key_filename, key_size} then | |
105 os.execute(("chmod 400 '%s'"):format(key_filename)); | |
106 pctl.show_message("Key written to %s", key_filename); | |
107 pposix.umask(old_umask); | |
108 return nil, key_filename; | |
109 end | |
110 pctl.show_message("There was a problem, see OpenSSL output"); | |
111 else | |
112 pctl.show_usage("cert key HOSTNAME <bits>", "Generates a RSA key named HOSTNAME.key\n " | |
113 .."Prompts for a key size if none given") | |
114 end | |
115 end | |
116 | |
117 function cert_commands.request(arg) | |
118 if #arg >= 1 and arg[1] ~= "--help" then | |
119 local req_filename = cert_basedir .. "/" .. arg[1] .. ".req"; | |
120 if use_existing(req_filename) then | |
121 return nil, req_filename; | |
122 end | |
123 local _, key_filename = cert_commands.key({arg[1]}); | |
124 local _, conf_filename = cert_commands.config(arg); | |
125 if openssl.req{new=true, key=key_filename, utf8=true, sha256=true, config=conf_filename, out=req_filename} then | |
126 pctl.show_message("Certificate request written to %s", req_filename); | |
127 else | |
128 pctl.show_message("There was a problem, see OpenSSL output"); | |
129 end | |
130 else | |
131 pctl.show_usage("cert request HOSTNAME [HOSTNAME+]", "Generates a certificate request for the supplied hostname(s)") | |
132 end | |
133 end | |
134 | |
135 function cert_commands.generate(arg) | |
136 if #arg >= 1 and arg[1] ~= "--help" then | |
137 local cert_filename = cert_basedir .. "/" .. arg[1] .. ".crt"; | |
138 if use_existing(cert_filename) then | |
139 return nil, cert_filename; | |
140 end | |
141 local _, key_filename = cert_commands.key({arg[1]}); | |
142 local _, conf_filename = cert_commands.config(arg); | |
143 if key_filename and conf_filename and cert_filename | |
144 and openssl.req{new=true, x509=true, nodes=true, key=key_filename, | |
145 days=365, sha256=true, utf8=true, config=conf_filename, out=cert_filename} then | |
146 pctl.show_message("Certificate written to %s", cert_filename); | |
147 print(); | |
148 else | |
149 pctl.show_message("There was a problem, see OpenSSL output"); | |
150 end | |
151 else | |
152 pctl.show_usage("cert generate HOSTNAME [HOSTNAME+]", "Generates a self-signed certificate for the current hostname(s)") | |
153 end | |
154 end | |
155 | |
156 local function sh_esc(s) | |
157 return "'" .. s:gsub("'", "'\\''") .. "'"; | |
158 end | |
159 | |
160 local function copy(from, to, umask, owner, group) | |
161 local old_umask = umask and pposix.umask(umask); | |
162 local attrs = lfs.attributes(to); | |
163 if attrs then -- Move old file out of the way | |
164 local backup = to..".bkp~"..os.date("%FT%T", attrs.change); | |
165 os.rename(to, backup); | |
166 end | |
167 -- FIXME friendlier error handling, maybe move above backup back? | |
168 local input = assert(io.open(from)); | |
169 local output = assert(io.open(to, "w")); | |
170 local data = input:read(2^11); | |
171 while data and output:write(data) do | |
172 data = input:read(2^11); | |
173 end | |
174 assert(input:close()); | |
175 assert(output:close()); | |
176 if not prosody.installed then | |
177 -- FIXME this is possibly specific to GNU chown | |
178 os.execute(("chown -c --reference=%s %s"):format(sh_esc(cert_basedir), sh_esc(to))); | |
179 elseif owner and group then | |
180 local ok = os.execute(("chown %s:%s %s"):format(sh_esc(owner), sh_esc(group), sh_esc(to))); | |
181 assert(ok == true or ok == 0, "Failed to change ownership of "..to); | |
182 end | |
183 if old_umask then pposix.umask(old_umask); end | |
184 return true; | |
185 end | |
186 | |
187 function cert_commands.import(arg) | |
188 local hostnames = {}; | |
189 -- Move hostname arguments out of arg, the rest should be a list of paths | |
190 while arg[1] and prosody.hosts[ arg[1] ] do | |
191 table.insert(hostnames, table.remove(arg, 1)); | |
192 end | |
193 if hostnames[1] == nil then | |
194 local domains = os.getenv"RENEWED_DOMAINS"; -- Set if invoked via certbot | |
195 if domains then | |
196 for host in domains:gmatch("%S+") do | |
197 table.insert(hostnames, host); | |
198 end | |
199 else | |
200 for host in pairs(prosody.hosts) do | |
201 if host ~= "*" and configmanager.get(host, "enabled") ~= false then | |
202 table.insert(hostnames, host); | |
203 end | |
204 end | |
205 end | |
206 end | |
207 if not arg[1] or arg[1] == "--help" then -- Probably forgot the path | |
208 pctl.show_usage("cert import [HOSTNAME+] /path/to/certs [/other/paths/]+", | |
209 "Copies certificates to "..cert_basedir); | |
210 return 1; | |
211 end | |
212 local owner, group; | |
213 if pposix.getuid() == 0 then -- We need root to change ownership | |
214 owner = configmanager.get("*", "prosody_user") or "prosody"; | |
215 group = configmanager.get("*", "prosody_group") or owner; | |
216 end | |
217 local cm = require "core.certmanager"; | |
218 local imported = {}; | |
219 for _, host in ipairs(hostnames) do | |
220 for _, dir in ipairs(arg) do | |
221 local paths = cm.find_cert(dir, host); | |
222 if paths then | |
223 copy(paths.certificate, cert_basedir .. "/" .. host .. ".crt", nil, owner, group); | |
224 copy(paths.key, cert_basedir .. "/" .. host .. ".key", "0377", owner, group); | |
225 table.insert(imported, host); | |
226 else | |
227 -- TODO Say where we looked | |
228 pctl.show_warning("No certificate for host "..host.." found :("); | |
229 end | |
230 -- TODO Additional checks | |
231 -- Certificate names matches the hostname | |
232 -- Private key matches public key in certificate | |
233 end | |
234 end | |
235 if imported[1] then | |
236 pctl.show_message("Imported certificate and key for hosts %s", table.concat(imported, ", ")); | |
237 local ok, err = pctl.reload(); | |
238 if not ok and err ~= "not-running" then | |
239 pctl.show_message(pctl.error_messages[err]); | |
240 end | |
241 else | |
242 pctl.show_warning("No certificates imported :("); | |
243 return 1; | |
244 end | |
245 end | |
246 | |
247 local function cert(arg) | |
248 if #arg >= 1 and arg[1] ~= "--help" then | |
249 openssl = require "util.openssl"; | |
250 lfs = require "lfs"; | |
251 local cert_dir_attrs = lfs.attributes(cert_basedir); | |
252 if not cert_dir_attrs then | |
253 pctl.show_warning("The directory "..cert_basedir.." does not exist"); | |
254 return 1; -- TODO Should we create it? | |
255 end | |
256 local uid = pposix.getuid(); | |
257 if uid ~= 0 and uid ~= cert_dir_attrs.uid then | |
258 pctl.show_warning("The directory "..cert_basedir.." is not owned by the current user, won't be able to write files to it"); | |
259 return 1; | |
260 elseif not cert_dir_attrs.permissions then -- COMPAT with LuaFilesystem < 1.6.2 (hey CentOS!) | |
261 pctl.show_message("Unable to check permissions on %s (LuaFilesystem 1.6.2+ required)", cert_basedir); | |
262 pctl.show_message("Please confirm that Prosody (and only Prosody) can write to this directory)"); | |
263 elseif cert_dir_attrs.permissions:match("^%.w..%-..%-.$") then | |
264 pctl.show_warning("The directory "..cert_basedir.." not only writable by its owner"); | |
265 return 1; | |
266 end | |
267 local subcmd = table.remove(arg, 1); | |
268 if type(cert_commands[subcmd]) == "function" then | |
269 if subcmd ~= "import" then -- hostnames are optional for import | |
270 if not arg[1] then | |
271 pctl.show_message"You need to supply at least one hostname" | |
272 arg = { "--help" }; | |
273 end | |
274 if arg[1] ~= "--help" and not prosody.hosts[arg[1]] then | |
275 pctl.show_message(pctl.error_messages["no-such-host"]); | |
276 return 1; | |
277 end | |
278 end | |
279 return cert_commands[subcmd](arg); | |
280 elseif subcmd == "check" then | |
281 return require "util.prosodyctl.check".check({"certs"}); | |
282 end | |
283 end | |
284 pctl.show_usage("cert config|request|generate|key|import", "Helpers for generating X.509 certificates and keys.") | |
285 for _, cmd in pairs(cert_commands) do | |
286 print() | |
287 cmd{ "--help" } | |
288 end | |
289 end | |
290 | |
291 return { | |
292 cert = cert; | |
293 }; |