Mercurial > prosody-hg
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 |
| parents | |
| children | 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 }; |
