diff options
-rw-r--r-- | .luacheckrc | 5 | ||||
-rw-r--r-- | GNUmakefile | 3 | ||||
-rw-r--r-- | core/certmanager.lua | 7 | ||||
-rw-r--r-- | core/portmanager.lua | 6 | ||||
-rw-r--r-- | net/server_epoll.lua | 2 | ||||
-rw-r--r-- | plugins/mod_admin_shell.lua | 57 | ||||
-rw-r--r-- | plugins/mod_s2s.lua | 24 | ||||
-rw-r--r-- | plugins/mod_tls.lua | 8 | ||||
-rwxr-xr-x | prosodyctl | 1 | ||||
-rw-r--r-- | spec/tls/README | 11 | ||||
-rwxr-xr-x | spec/tls/config1/assert.sh | 10 | ||||
-rwxr-xr-x | spec/tls/config1/prepare.sh | 14 | ||||
-rw-r--r-- | spec/tls/config1/prosody.cfg.lua | 6 | ||||
-rwxr-xr-x | spec/tls/config2/assert.sh | 10 | ||||
-rwxr-xr-x | spec/tls/config2/prepare.sh | 14 | ||||
-rw-r--r-- | spec/tls/config2/prosody.cfg.lua | 6 | ||||
-rwxr-xr-x | spec/tls/config3/assert.sh | 25 | ||||
-rwxr-xr-x | spec/tls/config3/prepare.sh | 28 | ||||
-rw-r--r-- | spec/tls/config3/prosody.cfg.lua | 28 | ||||
-rw-r--r-- | spec/tls/lib.sh | 45 | ||||
-rwxr-xr-x | spec/tls/run.sh | 37 | ||||
-rw-r--r-- | util/prosodyctl/check.lua | 2 |
22 files changed, 332 insertions, 17 deletions
diff --git a/.luacheckrc b/.luacheckrc index 09225d01..1a392dab 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -131,6 +131,11 @@ files["spec/"] = { std = "+busted"; globals = { "randomize" }; } +files["spec/tls"] = { + -- luacheck complains about the config files here, + -- but we don't really care about them + only = {}; +} files["prosody.cfg.lua"] = { ignore = { "131" }; globals = { diff --git a/GNUmakefile b/GNUmakefile index ec51c893..e47b258f 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -122,6 +122,9 @@ integration-test-%: all $(RUNWITH) prosodyctl --config ./spec/scansion/prosody.cfg.lua stop \ exit $$R +integration-test-tls: all + cd ./spec/tls && ./run.sh + coverage: -rm -- luacov.* $(BUSTED) --lua=$(RUNWITH) -c diff --git a/core/certmanager.lua b/core/certmanager.lua index 3acddf73..b13d57b3 100644 --- a/core/certmanager.lua +++ b/core/certmanager.lua @@ -116,14 +116,17 @@ local function index_certs(dir, files_by_name, depth_limit) else log("debug", "Skipping expired certificate: %s", full); end + else + log("debug", "Skipping non-certificate (based on contents): %s", full); end f:close(); elseif err then - log("debug", "Failed to open file for indexing: %s", full); + log("debug", "Skipping file due to error: %s", err); end + else + log("debug", "Skipping non-certificate (based on filename): %s", full); end end - log("debug", "Certificate index in %s: %q", dir, files_by_name); -- | hostname | filename | service | return files_by_name; end diff --git a/core/portmanager.lua b/core/portmanager.lua index 88bd7b61..3b9b8d67 100644 --- a/core/portmanager.lua +++ b/core/portmanager.lua @@ -253,12 +253,14 @@ local function add_sni_host(host, service) -- TODO should this be some generic thing? e.g. in the service definition alternate_host = config.get(host, "http_host"); end - local ssl, err, cfg = certmanager.create_context(alternate_host or host, "server", prefix_ssl_config, active_service.tls_cfg); + local autocert = certmanager.find_host_cert(alternate_host or host); + local ssl, err, cfg = certmanager.create_context(alternate_host or host, "server", prefix_ssl_config, autocert, active_service.tls_cfg); if not ssl then log("error", "Error creating TLS context for SNI host %s: %s", host, err); else + log("debug", "Using certificate %s for %s (%s) on %s (%s)", cfg.certificate, service or name, name, alternate_host or host, host) local ok, err = active_service.server:sslctx():set_sni_host( - host, + alternate_host or host, cfg.certificate, cfg.key ); diff --git a/net/server_epoll.lua b/net/server_epoll.lua index 44ab4f69..ca5a950c 100644 --- a/net/server_epoll.lua +++ b/net/server_epoll.lua @@ -772,7 +772,7 @@ function interface:starttls(tls_ctx) self.onreadable = interface.inittls; self:set(true, true); self:setreadtimeout(false); - self:setwritetimeout(cfg.ssl_handshake_timeout); + self:setwritetimeout(self._connected and cfg.ssl_handshake_timeout or cfg.connect_timeout); self:debug("Prepared to start TLS"); end end diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index c2b921b4..d6d082f3 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -2401,6 +2401,63 @@ function def_env.debug:async(runner_id) return true, ("%d runners pending"):format(c); end +describe_command [[debug:cert_index([path]) - Show Prosody's view of a directory of certs]] +function def_env.debug:cert_index(path) + local print = self.session.print; + local cm = require "core.certmanager"; + + path = path or module:get_option("certificates", "certs"); + + local sink = logger.add_simple_sink(function (source, level, message) + if source == "certmanager" then + if level == "debug" or level == "info" then + level = "II"; + elseif level == "warn" or level == "error" then + level = "EE"; + end + self.session.print(level..": "..message); + end + end); + + print("II: Scanning "..path.."..."); + + local index = {}; + cm.index_certs(path, index) + + if not logger.remove_sink(sink) then + module:log("warn", "Unable to remove log sink"); + end + + local c, max_domain = 0, 8; + for domain in pairs(index) do + if #domain > max_domain then + max_domain = #domain; + end + end + + print(""); + + local row = format_table({ + { title = "Domain", width = max_domain }; + { title = "Certificate", width = "100%" }; + { title = "Service", width = 5 }; + }, self.session.width); + print(row()); + print(("-"):rep(self.session.width or 80)); + for domain, certs in it.sorted_pairs(index) do + for cert_file, services in it.sorted_pairs(certs) do + for service in it.sorted_pairs(services) do + c = c + 1; + print(row({ domain, cert_file, service })); + end + end + end + + print(""); + + return true, ("Showing %d certificates in %s"):format(c, path); +end + def_env.stats = new_section("Commands to show internal statistics"); local short_units = { diff --git a/plugins/mod_s2s.lua b/plugins/mod_s2s.lua index 84ae34b5..5b81cf4f 100644 --- a/plugins/mod_s2s.lua +++ b/plugins/mod_s2s.lua @@ -995,16 +995,23 @@ end -- Complete the sentence "Your certificate " with what's wrong local function friendly_cert_error(session) --> string if session.cert_chain_status == "invalid" then + local cert_errors = set.new(); + if type(session.cert_chain_errors) == "table" then - local cert_errors = set.new(session.cert_chain_errors[1]); - if cert_errors:contains("certificate has expired") then - return "has expired"; - elseif cert_errors:contains("self signed certificate") then - return "is self-signed"; - elseif cert_errors:contains("no matching DANE TLSA records") then - return "does not match any DANE TLSA records"; - end + cert_errors:add_list(session.cert_chain_errors[1]); + elseif type(session.cert_chain_errors) == "string" then + cert_errors:add(session.cert_chain_errors); + end + if cert_errors:contains("certificate has expired") then + return "has expired"; + elseif cert_errors:contains("self signed certificate") or cert_errors:contains("self-signed certificate") then + return "is self-signed"; + elseif cert_errors:contains("no matching DANE TLSA records") then + return "does not match any DANE TLSA records"; + end + + if type(session.cert_chain_errors) == "table" then local chain_errors = set.new(session.cert_chain_errors[2]); for i, e in pairs(session.cert_chain_errors) do if i > 2 then chain_errors:add_list(e); end @@ -1015,7 +1022,6 @@ local function friendly_cert_error(session) --> string return "does not match any DANE TLSA records"; end end - -- TODO cert_chain_errors can be a string, handle that return "is not trusted"; -- for some other reason elseif session.cert_identity_status == "invalid" then return "is not valid for this name"; diff --git a/plugins/mod_tls.lua b/plugins/mod_tls.lua index b240a64c..a3af2f84 100644 --- a/plugins/mod_tls.lua +++ b/plugins/mod_tls.lua @@ -55,6 +55,7 @@ function module.load(reload) module:log("debug", "Creating context for c2s"); local request_client_certs = { verify = { "peer", "client_once", }; }; + local custom_cert_verification = { verifyext = { "lsec_continue", "lsec_ignore_purpose" }; }; local xmpp_alpn = { alpn = "xmpp-server" }; ssl_ctx_c2s, err_c2s, ssl_cfg_c2s = create_context(host.host, "server", host_c2s, host_ssl, global_c2s); -- for incoming client connections @@ -62,12 +63,15 @@ function module.load(reload) module:log("debug", "Creating context for s2sout"); -- for outgoing server connections - ssl_ctx_s2sout, err_s2sout, ssl_cfg_s2sout = create_context(host.host, "client", host_s2s, host_ssl, global_s2s, xmpp_alpn); + ssl_ctx_s2sout, err_s2sout, ssl_cfg_s2sout = create_context(host.host, "client", host_s2s, host_ssl, global_s2s, xmpp_alpn, + custom_cert_verification); if not ssl_ctx_s2sout then module:log("error", "Error creating contexts for s2sout: %s", err_s2sout); end module:log("debug", "Creating context for s2sin"); -- for incoming server connections - ssl_ctx_s2sin, err_s2sin, ssl_cfg_s2sin = create_context(host.host, "server", host_s2s, host_ssl, global_s2s, request_client_certs); + ssl_ctx_s2sin, err_s2sin, ssl_cfg_s2sin = create_context(host.host, "server", + host_s2s, host_ssl, global_s2s, request_client_certs, custom_cert_verification + ); if not ssl_ctx_s2sin then module:log("error", "Error creating contexts for s2sin: %s", err_s2sin); end if reload then @@ -195,6 +195,7 @@ local function service_command_warning(service_command) elseif init == "rc.d" then show_warning(" /etc/init.d/prosody %s", service_command); end + show_warning(""); else show_warning(" it may conflict with your system's service manager."); show_warning(""); diff --git a/spec/tls/README b/spec/tls/README new file mode 100644 index 00000000..58201728 --- /dev/null +++ b/spec/tls/README @@ -0,0 +1,11 @@ +These tests check that SSL/TLS configuration is working as expected. + +Just run ./run.sh in this directory (or from the top level, +`make integration-test-tls`. + +Known issues: + - The tests do not thorougly clean up after themselves (certs, logs, etc.). + This is partly intentional, so they can be inspected in case of failures. + - Certs are regenerated every time. Could be smarter about this. But it also + helps to guard against incorrect Prosody instances running and hogging the + ports, etc. diff --git a/spec/tls/config1/assert.sh b/spec/tls/config1/assert.sh new file mode 100755 index 00000000..f7d41c26 --- /dev/null +++ b/spec/tls/config1/assert.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +#set -x + +. ../lib.sh + +expect_cert "certs/example.com.crt" "localhost:5222" "example.com" "xmpp" +expect_cert "certs/share.example.com.crt" "localhost:5281" "share.example.com" "tls" + +exit "$failures" diff --git a/spec/tls/config1/prepare.sh b/spec/tls/config1/prepare.sh new file mode 100755 index 00000000..a8ec2822 --- /dev/null +++ b/spec/tls/config1/prepare.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +certs="./certs" + +for domain in {,share.}example.com; do + openssl req -x509 \ + -newkey rsa:4096 \ + -keyout "${certs}/${domain}.key" \ + -out "${certs}/${domain}.crt" \ + -sha256 \ + -days 365 \ + -nodes \ + -subj "/CN=${domain}" 2>/dev/null; +done diff --git a/spec/tls/config1/prosody.cfg.lua b/spec/tls/config1/prosody.cfg.lua new file mode 100644 index 00000000..9e94de58 --- /dev/null +++ b/spec/tls/config1/prosody.cfg.lua @@ -0,0 +1,6 @@ +Include "prosody-default.cfg.lua" + +VirtualHost "example.com" + enabled = true + +Component "share.example.com" "http_file_share" diff --git a/spec/tls/config2/assert.sh b/spec/tls/config2/assert.sh new file mode 100755 index 00000000..d1af0f51 --- /dev/null +++ b/spec/tls/config2/assert.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +#set -x + +. ../lib.sh + +expect_cert "certs/xmpp.example.com.crt" "localhost:5281" "xmpp.example.com" "tls" +expect_cert "certs/example.com.crt" "localhost:5222" "example.com" "xmpp" + +exit "$failures" diff --git a/spec/tls/config2/prepare.sh b/spec/tls/config2/prepare.sh new file mode 100755 index 00000000..1d67af4e --- /dev/null +++ b/spec/tls/config2/prepare.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +certs="./certs" + +for domain in {,xmpp.}example.com; do + openssl req -x509 \ + -newkey rsa:4096 \ + -keyout "${certs}/${domain}.key" \ + -out "${certs}/${domain}.crt" \ + -sha256 \ + -days 365 \ + -nodes \ + -subj "/CN=${domain}" 2>/dev/null; +done diff --git a/spec/tls/config2/prosody.cfg.lua b/spec/tls/config2/prosody.cfg.lua new file mode 100644 index 00000000..a5728516 --- /dev/null +++ b/spec/tls/config2/prosody.cfg.lua @@ -0,0 +1,6 @@ +Include "prosody-default.cfg.lua" + +VirtualHost "example.com" + enabled = true + modules_enabled = { "http" } + http_host = "xmpp.example.com" diff --git a/spec/tls/config3/assert.sh b/spec/tls/config3/assert.sh new file mode 100755 index 00000000..e36f7fb1 --- /dev/null +++ b/spec/tls/config3/assert.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +#set -x + +. ../lib.sh + +expect_cert "certs/xmpp.example.com.crt" "localhost:5281" "xmpp.example.com" "tls" +expect_cert "certs/example.com.crt" "localhost:5222" "example.com" "xmpp" +expect_cert "certs/example.com.crt" "localhost:5223" "example.com" "xmpps" + +# Weirdly configured host, just to test manual override behaviour +expect_cert "certs/example.com.crt" "localhost:5222" "example.net" "xmpp" +expect_cert "certs/example.com.crt" "localhost:5222" "example.net" "xmpp" +expect_cert "certs/example.com.crt" "localhost:5223" "example.net" "tls" +expect_cert "certs/example.com.crt" "localhost:5281" "example.net" "tls" + +# Three domains using a single cert with SANs +expect_cert "certs/example.org.crt" "localhost:5222" "example.org" "xmpp" +expect_cert "certs/example.org.crt" "localhost:5223" "example.org" "xmpps" +expect_cert "certs/example.org.crt" "localhost:5269" "example.org" "xmpp-server" +expect_cert "certs/example.org.crt" "localhost:5269" "share.example.org" "xmpp-server" +expect_cert "certs/example.org.crt" "localhost:5269" "groups.example.org" "xmpp-server" +expect_cert "certs/example.org.crt" "localhost:5281" "share.example.org" "tls" + +exit "$failures" diff --git a/spec/tls/config3/prepare.sh b/spec/tls/config3/prepare.sh new file mode 100755 index 00000000..89269d73 --- /dev/null +++ b/spec/tls/config3/prepare.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +certs="./certs" + +for domain in {,xmpp.}example.com example.net; do + openssl req -x509 \ + -newkey rsa:4096 \ + -keyout "${certs}/${domain}.key" \ + -out "${certs}/${domain}.crt" \ + -sha256 \ + -days 365 \ + -nodes \ + -quiet \ + -subj "/CN=${domain}" 2>/dev/null; +done + +for domain in example.org; do + openssl req -x509 \ + -newkey rsa:4096 \ + -keyout "${certs}/${domain}.key" \ + -out "${certs}/${domain}.crt" \ + -sha256 \ + -days 365 \ + -nodes \ + -subj "/CN=${domain}" \ + -addext "subjectAltName = DNS:${domain}, DNS:groups.${domain}, DNS:share.${domain}" \ + 2>/dev/null; +done diff --git a/spec/tls/config3/prosody.cfg.lua b/spec/tls/config3/prosody.cfg.lua new file mode 100644 index 00000000..a92dbfa8 --- /dev/null +++ b/spec/tls/config3/prosody.cfg.lua @@ -0,0 +1,28 @@ +Include "prosody-default.cfg.lua" + +c2s_direct_tls_ports = { 5223 } + +VirtualHost "example.com" + enabled = true + modules_enabled = { "http" } + http_host = "xmpp.example.com" + +VirtualHost "example.net" + ssl = { + certificate = "certs/example.com.crt"; + key = "certs/example.com.key"; + } + + https_ssl = { + certificate = "certs/example.com.crt"; + key = "certs/example.com.key"; + } + + c2s_direct_tls_ssl = { + certificate = "certs/example.com.crt"; + key = "certs/example.com.key"; + } + +VirtualHost "example.org" +Component "share.example.org" "http_file_share" +Component "groups.example.org" "muc" diff --git a/spec/tls/lib.sh b/spec/tls/lib.sh new file mode 100644 index 00000000..d072802a --- /dev/null +++ b/spec/tls/lib.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +test_name="$(basename "$PWD")" +export failures=0 + +get_net_cert () { + address="${1?}" + sni="${2?}" + proto="${3?}" + local flags=() + case "$proto" in + "xmpp") flags=(-starttls xmpp -name "$sni");; + "xmpps") flags=(-alpn xmpp-client);; + "xmpp-server") flags=(-starttls xmpp-server -name "$sni");; + "xmpps-server") flags=(-alpn xmpp-server);; + "tls") ;; + *) printf "EE: Unknown protocol: %s\n" "$proto" >&2; exit 1;; + esac + openssl s_client -connect "$address" -servername "$sni" "${flags[@]}" 2>/dev/null </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' +} + +get_file_cert () { + fn="${1?}" + sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' "$fn" +} + +expect_cert () { + fn="${1?}" + address="${2?}" + sni="${3?}" + proto="${4?}" + net_cert="$(get_net_cert "$address" "$sni" "$proto")" + file_cert="$(get_file_cert "$fn")" + if [[ "$file_cert" != "$net_cert" ]]; then + echo "---" + echo "NOT OK: $test_name: Expected $fn on $address (SNI $sni)" + echo "Received:" + openssl x509 -in <(echo "$net_cert") -text + echo "---" + failures=1; + return 1; + fi + echo "OK: $test_name: $fn observed on $address (SNI $sni)" + return 0; +} diff --git a/spec/tls/run.sh b/spec/tls/run.sh new file mode 100755 index 00000000..8bceddb2 --- /dev/null +++ b/spec/tls/run.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +export LUA_PATH="../../../?.lua;;" +export LUA_CPATH="../../../?.so;;" + +any_failed=0 + +for config in config*; do + echo "# Preparing $config" + pushd "$config"; + cp ../../../prosody.cfg.lua.dist ./prosody-default.cfg.lua + echo 'VirtualHost "*" {pidfile = "prosody.pid";log={debug="prosody.log"}}' >> ./prosody-default.cfg.lua + ln -s ../../../plugins plugins + mkdir -p certs data + ./prepare.sh + ../../../prosody -D + sleep 1; + echo "# Testing $config" + ./assert.sh + status=$? + ../../../prosodyctl stop + rm plugins #prosody-default.cfg.lua + popd + if [[ "$status" != "0" ]]; then + echo -n "NOT "; + any_failed=1 + fi + echo "OK: $config"; +done + +if [[ "$any_failed" != "0" ]]; then + echo "NOT OK: One or more TLS tests failed"; + exit 1; +fi + +echo "OK: All TLS tests passed"; +exit 0; diff --git a/util/prosodyctl/check.lua b/util/prosodyctl/check.lua index 75ff5da4..622e475e 100644 --- a/util/prosodyctl/check.lua +++ b/util/prosodyctl/check.lua @@ -1313,7 +1313,7 @@ local function check(arg) http_loaded = false; end if http_loaded and not x509_verify_identity(http_host, nil, cert) then - print(" Not valid for HTTPS connections to "..host..".") + print(" Not valid for HTTPS connections to "..http_host..".") cert_ok = false end if use_dane then |