changeset 14221:565debd9c37a

Merge 13.0->trunk
author Matthew Wild <mwild1@gmail.com>
date Fri, 12 Jun 2026 13:05:10 +0100
parents 2ec517d8d43e (current diff) 8f34af6a3821 (diff)
children ad7f7b5f4792
files core/certmanager.lua core/features.lua util/human/io.lua util/prosodyctl/check.lua
diffstat 7 files changed, 550 insertions(+), 84 deletions(-) [+]
line wrap: on
line diff
--- a/core/certmanager.lua	Sun Jun 07 20:32:55 2026 +0200
+++ b/core/certmanager.lua	Fri Jun 12 13:05:10 2026 +0100
@@ -12,6 +12,8 @@
 local tls = require "prosody.net.tls_luasec";
 local stat = require "lfs".attributes;
 
+local tlsref = require "prosody.util.tlsref";
+
 local x509 = require "prosody.util.x509";
 local lfs = require "lfs";
 
@@ -216,84 +218,6 @@
 	dane = tls.features.capabilities.dane and configmanager.get("*", "use_dane") ~= false and { "no_ee_namechecks" };
 }
 
--- https://datatracker.ietf.org/doc/html/rfc7919#appendix-A.1
-local ffdhe2048 = [[
------BEGIN DH PARAMETERS-----
-MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz
-+8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a
-87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7
-YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi
-7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD
-ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg==
------END DH PARAMETERS-----
-]]
-
-local mozilla_ssl_configs = {
-	-- https://wiki.mozilla.org/Security/Server_Side_TLS
-	-- Version 5.7 as of 2023-07-09
-	modern = {
-		protocol = "tlsv1_3";
-		options = { cipher_server_preference = false };
-		ciphers = "DEFAULT"; -- TLS 1.3 uses 'ciphersuites' rather than these
-		curveslist = { "X25519"; "prime256v1"; "secp384r1" };
-		ciphersuites = { "TLS_AES_128_GCM_SHA256"; "TLS_AES_256_GCM_SHA384"; "TLS_CHACHA20_POLY1305_SHA256" };
-	};
-	intermediate = {
-		protocol = "tlsv1_2+";
-		dhparam = ffdhe2048;
-		options = { cipher_server_preference = false };
-		ciphers = {
-			"ECDHE-ECDSA-AES128-GCM-SHA256";
-			"ECDHE-RSA-AES128-GCM-SHA256";
-			"ECDHE-ECDSA-AES256-GCM-SHA384";
-			"ECDHE-RSA-AES256-GCM-SHA384";
-			"ECDHE-ECDSA-CHACHA20-POLY1305";
-			"ECDHE-RSA-CHACHA20-POLY1305";
-			"DHE-RSA-AES128-GCM-SHA256";
-			"DHE-RSA-AES256-GCM-SHA384";
-			"DHE-RSA-CHACHA20-POLY1305";
-		};
-		curveslist = { "X25519"; "prime256v1"; "secp384r1" };
-		ciphersuites = { "TLS_AES_128_GCM_SHA256"; "TLS_AES_256_GCM_SHA384"; "TLS_CHACHA20_POLY1305_SHA256" };
-	};
-	old = {
-		protocol = "tlsv1+";
-		dhparam = nil; -- openssl dhparam 1024
-		options = { cipher_server_preference = true };
-		ciphers = {
-			"ECDHE-ECDSA-AES128-GCM-SHA256";
-			"ECDHE-RSA-AES128-GCM-SHA256";
-			"ECDHE-ECDSA-AES256-GCM-SHA384";
-			"ECDHE-RSA-AES256-GCM-SHA384";
-			"ECDHE-ECDSA-CHACHA20-POLY1305";
-			"ECDHE-RSA-CHACHA20-POLY1305";
-			"DHE-RSA-AES128-GCM-SHA256";
-			"DHE-RSA-AES256-GCM-SHA384";
-			"DHE-RSA-CHACHA20-POLY1305";
-			"ECDHE-ECDSA-AES128-SHA256";
-			"ECDHE-RSA-AES128-SHA256";
-			"ECDHE-ECDSA-AES128-SHA";
-			"ECDHE-RSA-AES128-SHA";
-			"ECDHE-ECDSA-AES256-SHA384";
-			"ECDHE-RSA-AES256-SHA384";
-			"ECDHE-ECDSA-AES256-SHA";
-			"ECDHE-RSA-AES256-SHA";
-			"DHE-RSA-AES128-SHA256";
-			"DHE-RSA-AES256-SHA256";
-			"AES128-GCM-SHA256";
-			"AES256-GCM-SHA384";
-			"AES128-SHA256";
-			"AES256-SHA256";
-			"AES128-SHA";
-			"AES256-SHA";
-			"DES-CBC3-SHA";
-		};
-		curveslist = { "X25519"; "prime256v1"; "secp384r1" };
-		ciphersuites = { "TLS_AES_128_GCM_SHA256"; "TLS_AES_256_GCM_SHA384"; "TLS_CHACHA20_POLY1305_SHA256" };
-	};
-};
-
-
 if tls.features.curves then
 	for i = #core_defaults.curveslist, 1, -1 do
 		if not tls.features.curves[ core_defaults.curveslist[i] ] then
@@ -321,12 +245,33 @@
 		-- We can't read the password interactively when daemonized
 		password = function() log("error", "Encrypted certificate for %s requires 'ssl' 'password' to be set in config", host); end;
 	});
-	local profile = configmanager.get("*", "tls_profile") or "intermediate";
-	if mozilla_ssl_configs[profile] then
-		cfg:apply(mozilla_ssl_configs[profile]);
-	elseif profile ~= "legacy" then
-		log("error", "Invalid value for 'tls_profile': expected one of \"modern\", \"intermediate\" (default), \"old\" or \"legacy\" but got %q", profile);
-		return nil, "Invalid configuration, 'tls_profile' had an unknown value.";
+	local profile_version = configmanager.get("*", "tls_profile_version");
+	local profile_name = configmanager.get("*", "tls_profile");
+
+	if profile_name ~= "legacy" then
+		local tls_profile, tls_profile_err = tlsref.get_profile(profile_version, profile_name);
+		if not tls_profile then
+			if tls_profile_err == "unknown-version" then
+				log(
+					"error",
+					"Invalid value %q for 'tls_profile_version': expected one of: \"%s\" or \"latest\"",
+					profile_version,
+					tlsref.get_valid_versions():concat("\", \"")
+				);
+				return nil, "Invalid configuration, 'tls_profile_version' had an unknown value.";
+			elseif tls_profile_err == "unknown-profile" then
+				log(
+					"error",
+					"Invalid value %q for 'tls_profile': expected one of \"%s\" or \"legacy\" (default: %q)",
+					profile_name,
+					tlsref.get_valid_profile_names(profile_version):concat("\", \""),
+					tlsref.get_default_profile_name(profile_version)
+				);
+				return nil, "Invalid configuration, 'tls_profile' had an unknown value.";
+			end
+		else
+			cfg:apply(tls_profile);
+		end
 	end
 	cfg:apply(global_ssl_config);
 
--- a/core/features.lua	Sun Jun 07 20:32:55 2026 +0200
+++ b/core/features.lua	Fri Jun 12 13:05:10 2026 +0100
@@ -39,5 +39,8 @@
 
 		-- SIGUSR1 and 2 events
 		"signal-events";
+
+		-- util.human.io.simple_version_compare
+		"simple-version-compare";
 	};
 };
--- a/spec/util_human_io_spec.lua	Sun Jun 07 20:32:55 2026 +0200
+++ b/spec/util_human_io_spec.lua	Fri Jun 12 13:05:10 2026 +0100
@@ -100,6 +100,79 @@
 			return assert.is_nil(human_io.parse_duration_lax("two weeks"), "\"2 weeks\" -> nil");
 		end);
 	end);
+
+	describe("simple_version_compare", function ()
+		local version_compare = human_io.simple_version_compare;
+
+		local function cmp_version(a, b, exp)
+			assert.equal(exp, version_compare(a, b), ("%q < %q"):format(a, b));
+		end
+
+		it("orders versions with differing major numbers", function ()
+			cmp_version("1.0", "2.0", true);
+			cmp_version("2.0", "1.0", false);
+		end);
+
+		it("orders versions with differing minor numbers", function ()
+			cmp_version("1.0", "1.1", true);
+			cmp_version("1.1", "1.0", false);
+		end);
+
+		it("treats a shorter version as less than a longer one sharing the same prefix", function ()
+			cmp_version("1.0", "1.0.1", true);
+			cmp_version("1.0.1", "1.0", false);
+
+			cmp_version("1", "1.0", true);
+			cmp_version("1", "1.1", true);
+
+			cmp_version("1.0.0", "1.0.0.1", true);
+		end);
+
+		it("compares components left-to-right regardless of how many components there are", function ()
+			cmp_version("1.0.1", "1.1", true);
+			cmp_version("1.1", "1.0.1", false);
+
+			cmp_version("1.0.1", "1.1.0", true);
+			cmp_version("1.1.0", "1.0.1", false);
+
+			cmp_version("1.0.0.1", "1.0.1", true);
+		end);
+
+		it("never considers a version less than itself", function ()
+			cmp_version("0", "0", false);
+			cmp_version("1.0", "1.0", false);
+			cmp_version("1.2.3", "1.2.3", false);
+			cmp_version("foo", "foo", false);
+		end);
+
+		it("compares components numerically", function ()
+			cmp_version("1.9", "1.10", true);
+			cmp_version("1.10", "1.9", false);
+
+			cmp_version("1.9.9", "1.10.0", true);
+
+			cmp_version("2.0", "10.0", true);
+			cmp_version("10.0", "2.0", false);
+
+			cmp_version("1.2", "1.10", true);
+
+			cmp_version("1.99", "1.100", true);
+			cmp_version("1.100", "1.99", false);
+		end);
+
+		it("handles leading zeros in numeric components", function ()
+			cmp_version("1.02", "1.10", true);
+			cmp_version("1.10", "1.02", false);
+		end);
+
+		it("treats an empty or non-numeric string as a version with no components", function ()
+			cmp_version("", "1.0", true);
+			cmp_version("1.0", "", false);
+			cmp_version("foo", "1.0", true);
+			cmp_version("1.0", "foo", false);
+			cmp_version("", "", false);
+		end);
+	end);
 end);
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/spec/util_tlsref_spec.lua	Fri Jun 12 13:05:10 2026 +0100
@@ -0,0 +1,168 @@
+local tlsref = require "prosody.util.tlsref";
+
+describe("util.tlsref", function()
+	local version_compare = require "prosody.util.human.io".simple_version_compare;
+
+	describe("default version", function()
+		it("is not greater than latest version", function ()
+			local default_version = tlsref.get_default_version();
+			local latest_version = tlsref.get_latest_version();
+			assert.equal(false, version_compare(latest_version, default_version));
+		end);
+
+		it("is a known version", function ()
+			assert.is_table(tlsref.get_profile(tlsref.get_default_version()));
+		end);
+	end);
+
+	describe("latest version", function ()
+		it("is a known version", function ()
+			assert.is_table(tlsref.get_profile("latest"));
+		end);
+
+		it("is the first entry of get_valid_versions()", function ()
+			local versions = tlsref.get_valid_versions();
+			assert.equal(tlsref.get_latest_version(), versions[1]);
+		end);
+	end);
+
+	describe("get_valid_versions()", function ()
+		it("returns versions in descending order", function ()
+			local versions = tlsref.get_valid_versions();
+			assert.is_true(#versions >= 1);
+			for i = 1, #versions-1 do
+				-- versions[i] must sort before versions[i+1]
+				assert.equal(false, version_compare(versions[i], versions[i+1]));
+			end
+		end);
+
+		it("contains the default version", function ()
+			local found = false;
+			for _, v in ipairs(tlsref.get_valid_versions()) do
+				if v == tlsref.get_default_version() then found = true; end
+			end
+			assert.is_true(found);
+		end);
+	end);
+
+	describe("get_valid_profile_names()", function ()
+		it("returns an error for unknown versions", function ()
+			local names, err = tlsref.get_valid_profile_names("0.0");
+			assert.is_nil(names);
+			assert.equal("unknown-version", err);
+		end);
+
+		it("uses the default version when none is given", function ()
+			assert.same(
+				tlsref.get_valid_profile_names(tlsref.get_default_version()),
+				tlsref.get_valid_profile_names()
+			);
+		end);
+
+		it("accepts 'latest' as a version", function ()
+			assert.same(
+				tlsref.get_valid_profile_names(tlsref.get_latest_version()),
+				tlsref.get_valid_profile_names("latest")
+			);
+		end);
+
+		it("lists the most-preferred profile first", function ()
+			for _, version in ipairs(tlsref.get_valid_versions()) do
+				local names = tlsref.get_valid_profile_names(version);
+				assert.equal("modern", names[1],
+					("most-preferred profile should be first for version %s"):format(version));
+			end
+		end);
+	end);
+
+	describe("get_profile()", function ()
+		it("returns the default profile of the default version when called with no arguments", function ()
+			assert.equal(
+				tlsref.get_profile(tlsref.get_default_version(), tlsref.get_default_profile_name()),
+				tlsref.get_profile()
+			);
+		end);
+
+		it("resolves 'latest' to the latest version", function ()
+			assert.equal(
+				tlsref.get_profile(tlsref.get_latest_version()),
+				tlsref.get_profile("latest")
+			);
+		end);
+
+		it("returns an error for unknown versions", function ()
+			local profile, err = tlsref.get_profile("0.0");
+			assert.is_nil(profile);
+			assert.equal("unknown-version", err);
+		end);
+
+		it("returns an error for unknown profile names", function ()
+			local profile, err = tlsref.get_profile(nil, "no-such-profile");
+			assert.is_nil(profile);
+			assert.equal("unknown-profile", err);
+		end);
+
+		it("returns an error for profiles absent from a given version", function ()
+			-- 'old' was dropped from the 6.0 guidelines
+			local profile, err = tlsref.get_profile("6.0", "old");
+			assert.is_nil(profile);
+			assert.equal("unknown-profile", err);
+		end);
+
+		it("resolves every advertised version/profile combination", function ()
+			for _, version in ipairs(tlsref.get_valid_versions()) do
+				for _, name in ipairs(tlsref.get_valid_profile_names(version)) do
+					local profile, err = tlsref.get_profile(version, name);
+					assert.is_table(profile,
+						("get_profile(%q, %q) failed: %s"):format(version, name, err));
+				end
+			end
+		end);
+	end);
+
+	describe("get_default_profile_name()", function ()
+		it("returns an error for unknown versions", function ()
+			local name, err = tlsref.get_default_profile_name("0.0");
+			assert.is_nil(name);
+			assert.equal("unknown-version", err);
+		end);
+
+		it("accepts 'latest' as a version", function ()
+			assert.is_string(tlsref.get_default_profile_name("latest"));
+		end);
+
+		it("returns a profile name valid for every version", function ()
+			for _, version in ipairs(tlsref.get_valid_versions()) do
+				local name = tlsref.get_default_profile_name(version);
+				assert.is_string(name);
+				assert.is_table(tlsref.get_profile(version, name),
+					("default profile %q missing from version %s"):format(name, version));
+			end
+		end);
+	end);
+
+	describe("profile contents", function ()
+		it("every profile specifies a TLS protocol version", function ()
+			for _, version in ipairs(tlsref.get_valid_versions()) do
+				for _, name in ipairs(tlsref.get_valid_profile_names(version)) do
+					local profile = tlsref.get_profile(version, name);
+					assert.is_string(profile.protocol);
+					assert.truthy(profile.protocol:match("^tlsv1"),
+						("unexpected protocol %q in %s/%s"):format(profile.protocol, version, name));
+				end
+			end
+		end);
+
+		it("every profile specifies TLS 1.3 ciphersuites and curves", function ()
+			for _, version in ipairs(tlsref.get_valid_versions()) do
+				for _, name in ipairs(tlsref.get_valid_profile_names(version)) do
+					local profile = tlsref.get_profile(version, name);
+					assert.is_table(profile.ciphersuites);
+					assert.is_true(#profile.ciphersuites > 0);
+					assert.is_table(profile.curveslist);
+					assert.is_true(#profile.curveslist > 0);
+				end
+			end
+		end);
+	end);
+end);
--- a/util/human/io.lua	Sun Jun 07 20:32:55 2026 +0200
+++ b/util/human/io.lua	Fri Jun 12 13:05:10 2026 +0100
@@ -235,6 +235,29 @@
 	return tonumber(n) * ( multipliers_lax[m] or 1 );
 end
 
+-- Compares simple version strings (numeric only, not semver compatible)
+-- (can be passed to table.sort)
+local function simple_version_compare(a, b)
+	local a_f, a_s, a_part = a:gmatch("%d+");
+	local b_f, b_s, b_part = b:gmatch("%d+");
+
+	a_part = a_f(a_s, a_part);
+	b_part = b_f(b_s, b_part);
+
+	while a_part and b_part do
+		local an, bn = tonumber(a_part), tonumber(b_part);
+		if an ~= bn then
+			return an < bn;
+		end
+		a_part = a_f(a_s, a_part);
+		b_part = b_f(b_s, b_part);
+	end
+
+	-- return true (a is lower) if b is exhausted
+	-- otherwise, they are equal or b is longer/greater (-> return false)
+	return b_part ~= nil;
+end
+
 return {
 	getchar = getchar;
 	getline = getline;
@@ -250,4 +273,5 @@
 	table = new_table;
 	parse_duration = parse_duration;
 	parse_duration_lax = parse_duration_lax;
+	simple_version_compare = simple_version_compare;
 };
--- a/util/prosodyctl/check.lua	Sun Jun 07 20:32:55 2026 +0200
+++ b/util/prosodyctl/check.lua	Fri Jun 12 13:05:10 2026 +0100
@@ -465,6 +465,7 @@
 			"statistics_interval",
 			"tcp_keepalives",
 			"tls_profile",
+			"tls_profile_version",
 			"trusted_proxies",
 			"umask",
 			"use_dane",
@@ -733,6 +734,26 @@
 		end
 
 		do
+			local tlsref = require "prosody.util.tlsref";
+			local tls_profile_name = configmanager.get("*", "tls_profile");
+			local tls_profile_version = configmanager.get("*", "tls_profile_version");
+			local profile, err = tlsref.get_profile(tls_profile_version, tls_profile_name);
+			if not profile then
+				print("");
+				print("    TLS profile selection:");
+				if err == "unknown-version" then
+					print(("        tls_profile_version is set to an unknown value (%q)"):format(tls_profile_version));
+					print(("        Valid values: \"%s\""):format(tlsref.get_valid_versions():concat("\", \"")));
+				elseif err == "unknown-profile" then
+					print(("        tls_profile is set to an unknown value (%q)"):format(tls_profile_name));
+					print(("        Valid values: \"%s\""):format(tlsref.get_valid_profile_names():concat("\", \"")));
+				else
+					print(("        Unknown error loading the configured profile: %s"):format(err));
+				end
+			end
+		end
+
+		do
 			local registration_enabled_hosts = {};
 			for host in enabled_hosts() do
 				local host_modules, component = modulemanager.get_modules_for_host(host);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/tlsref.lua	Fri Jun 12 13:05:10 2026 +0100
@@ -0,0 +1,232 @@
+local array = require "prosody.util.array";
+local it = require "prosody.util.iterators";
+local version_compare = require "prosody.util.human.io".simple_version_compare;
+
+local latest_version = "6.0"
+local default_version = "5.7";
+local default_profile = "intermediate"; -- should exist in all profiles
+
+-- https://datatracker.ietf.org/doc/html/rfc7919#appendix-A.1
+local ffdhe2048 = [[
+-----BEGIN DH PARAMETERS-----
+MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz
++8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a
+87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7
+YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi
+7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD
+ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg==
+-----END DH PARAMETERS-----
+]];
+
+local profiles = {
+	-- https://wiki.mozilla.org/Security/Server_Side_TLS
+	["6.0"] = {
+		-- Version 6.0 as of 2026-04-08
+		modern = {
+			protocol = "tlsv1_3";
+			options = { cipher_server_preference = false };
+			ciphers = "DEFAULT"; -- TLS 1.3 uses 'ciphersuites' rather than these
+			curveslist = { "X25519MLKEM768"; "X25519"; "prime256v1"; "secp384r1" };
+			ciphersuites = { "TLS_AES_128_GCM_SHA256"; "TLS_AES_256_GCM_SHA384"; "TLS_CHACHA20_POLY1305_SHA256" };
+		};
+		intermediate = {
+			protocol = "tlsv1_2+";
+			dhparam = ffdhe2048;
+			options = { cipher_server_preference = false };
+			ciphers = {
+				"ECDHE-ECDSA-AES128-GCM-SHA256";
+				"ECDHE-RSA-AES128-GCM-SHA256";
+				"ECDHE-ECDSA-AES256-GCM-SHA384";
+				"ECDHE-RSA-AES256-GCM-SHA384";
+				"ECDHE-ECDSA-CHACHA20-POLY1305";
+				"ECDHE-RSA-CHACHA20-POLY1305";
+			};
+			curveslist = { "X25519MLKEM768"; "X25519"; "prime256v1"; "secp384r1" };
+			ciphersuites = { "TLS_AES_128_GCM_SHA256"; "TLS_AES_256_GCM_SHA384"; "TLS_CHACHA20_POLY1305_SHA256" };
+		};
+	};
+
+	["5.8"] = {
+		-- Version 5.8 as of 2026-04-08
+		modern = {
+			protocol = "tlsv1_3";
+			options = { cipher_server_preference = false };
+			ciphers = "DEFAULT"; -- TLS 1.3 uses 'ciphersuites' rather than these
+			curveslist = { "X25519MLKEM768"; "X25519"; "prime256v1"; "secp384r1" };
+			ciphersuites = { "TLS_AES_128_GCM_SHA256"; "TLS_AES_256_GCM_SHA384"; "TLS_CHACHA20_POLY1305_SHA256" };
+		};
+		intermediate = {
+			protocol = "tlsv1_2+";
+			dhparam = ffdhe2048;
+			options = { cipher_server_preference = false };
+			ciphers = {
+				"ECDHE-ECDSA-AES128-GCM-SHA256";
+				"ECDHE-RSA-AES128-GCM-SHA256";
+				"ECDHE-ECDSA-AES256-GCM-SHA384";
+				"ECDHE-RSA-AES256-GCM-SHA384";
+				"ECDHE-ECDSA-CHACHA20-POLY1305";
+				"ECDHE-RSA-CHACHA20-POLY1305";
+			};
+			curveslist = { "X25519MLKEM768"; "X25519"; "prime256v1"; "secp384r1" };
+			ciphersuites = { "TLS_AES_128_GCM_SHA256"; "TLS_AES_256_GCM_SHA384"; "TLS_CHACHA20_POLY1305_SHA256" };
+		};
+		old = {
+			protocol = "tlsv1+";
+			dhparam = ffdhe2048;
+			options = { cipher_server_preference = true };
+			ciphers = {
+				"ECDHE-ECDSA-AES128-GCM-SHA256";
+				"ECDHE-RSA-AES128-GCM-SHA256";
+				"ECDHE-ECDSA-AES256-GCM-SHA384";
+				"ECDHE-RSA-AES256-GCM-SHA384";
+				"ECDHE-ECDSA-CHACHA20-POLY1305";
+				"ECDHE-RSA-CHACHA20-POLY1305";
+				"ECDHE-ECDSA-AES128-SHA256";
+				"ECDHE-RSA-AES128-SHA256";
+				"ECDHE-ECDSA-AES128-SHA";
+				"ECDHE-RSA-AES128-SHA";
+				"ECDHE-ECDSA-AES256-SHA384";
+				"ECDHE-RSA-AES256-SHA384";
+				"ECDHE-ECDSA-AES256-SHA";
+				"ECDHE-RSA-AES256-SHA";
+				"AES128-GCM-SHA256";
+				"AES256-GCM-SHA384";
+				"AES128-SHA256";
+				"AES256-SHA256";
+				"AES128-SHA";
+				"AES256-SHA";
+				"DES-CBC3-SHA";
+			};
+			curveslist = { "X25519MLKEM768"; "X25519"; "prime256v1"; "secp384r1" };
+			ciphersuites = { "TLS_AES_128_GCM_SHA256"; "TLS_AES_256_GCM_SHA384"; "TLS_CHACHA20_POLY1305_SHA256" };
+		};
+	};
+
+	["5.7"] = {
+		-- Version 5.7 as of 2023-07-09
+		modern = {
+			protocol = "tlsv1_3";
+			options = { cipher_server_preference = false };
+			ciphers = "DEFAULT"; -- TLS 1.3 uses 'ciphersuites' rather than these
+			curveslist = { "X25519"; "prime256v1"; "secp384r1" };
+			ciphersuites = { "TLS_AES_128_GCM_SHA256"; "TLS_AES_256_GCM_SHA384"; "TLS_CHACHA20_POLY1305_SHA256" };
+		};
+		intermediate = {
+			protocol = "tlsv1_2+";
+			dhparam = ffdhe2048;
+			options = { cipher_server_preference = false };
+			ciphers = {
+				"ECDHE-ECDSA-AES128-GCM-SHA256";
+				"ECDHE-RSA-AES128-GCM-SHA256";
+				"ECDHE-ECDSA-AES256-GCM-SHA384";
+				"ECDHE-RSA-AES256-GCM-SHA384";
+				"ECDHE-ECDSA-CHACHA20-POLY1305";
+				"ECDHE-RSA-CHACHA20-POLY1305";
+				"DHE-RSA-AES128-GCM-SHA256";
+				"DHE-RSA-AES256-GCM-SHA384";
+				"DHE-RSA-CHACHA20-POLY1305";
+			};
+			curveslist = { "X25519"; "prime256v1"; "secp384r1" };
+			ciphersuites = { "TLS_AES_128_GCM_SHA256"; "TLS_AES_256_GCM_SHA384"; "TLS_CHACHA20_POLY1305_SHA256" };
+		};
+		old = {
+			protocol = "tlsv1+";
+			dhparam = nil; -- openssl dhparam 1024
+			options = { cipher_server_preference = true };
+			ciphers = {
+				"ECDHE-ECDSA-AES128-GCM-SHA256";
+				"ECDHE-RSA-AES128-GCM-SHA256";
+				"ECDHE-ECDSA-AES256-GCM-SHA384";
+				"ECDHE-RSA-AES256-GCM-SHA384";
+				"ECDHE-ECDSA-CHACHA20-POLY1305";
+				"ECDHE-RSA-CHACHA20-POLY1305";
+				"DHE-RSA-AES128-GCM-SHA256";
+				"DHE-RSA-AES256-GCM-SHA384";
+				"DHE-RSA-CHACHA20-POLY1305";
+				"ECDHE-ECDSA-AES128-SHA256";
+				"ECDHE-RSA-AES128-SHA256";
+				"ECDHE-ECDSA-AES128-SHA";
+				"ECDHE-RSA-AES128-SHA";
+				"ECDHE-ECDSA-AES256-SHA384";
+				"ECDHE-RSA-AES256-SHA384";
+				"ECDHE-ECDSA-AES256-SHA";
+				"ECDHE-RSA-AES256-SHA";
+				"DHE-RSA-AES128-SHA256";
+				"DHE-RSA-AES256-SHA256";
+				"AES128-GCM-SHA256";
+				"AES256-GCM-SHA384";
+				"AES128-SHA256";
+				"AES256-SHA256";
+				"AES128-SHA";
+				"AES256-SHA";
+				"DES-CBC3-SHA";
+			};
+			curveslist = { "X25519"; "prime256v1"; "secp384r1" };
+			ciphersuites = { "TLS_AES_128_GCM_SHA256"; "TLS_AES_256_GCM_SHA384"; "TLS_CHACHA20_POLY1305_SHA256" };
+		};
+	};
+};
+
+local function get_valid_versions()
+	return array.collect(it.keys(profiles)):sort(version_compare):reverse();
+end
+
+local pref_order = { modern = 100, intermediate = 50, old = 0 };
+
+local function sort_profile_by_pref(a, b)
+	local pref_a, pref_b = pref_order[a] or 0, pref_order[b] or 0;
+	return pref_a > pref_b; -- Best first
+end
+
+local function get_valid_profile_names(version)
+	if version == "latest" then
+		version = latest_version;
+	end
+	local version_profiles = profiles[version or default_version];
+	if not version_profiles then
+		return nil, "unknown-version";
+	end
+	return array.collect(it.keys(version_profiles)):sort(sort_profile_by_pref);
+end
+
+local function get_profile(version, profile_name)
+	if version == "latest" then
+		version = latest_version;
+	end
+	local version_profiles = profiles[version or default_version];
+	if not version_profiles then
+		return nil, "unknown-version";
+	end
+	local profile = version_profiles[profile_name or default_profile];
+	if not profile then
+		return nil, "unknown-profile";
+	end
+	return profile;
+end
+
+local function get_default_version()
+	return default_version;
+end
+
+local function get_latest_version()
+	return latest_version;
+end
+
+local function get_default_profile_name(version)
+	-- Default profile name is currently the same across all versions,
+	-- but can't guarantee this in the future - ensure valid version
+	if version ~= "latest" and not profiles[version or default_version] then
+		return nil, "unknown-version";
+	end
+	return default_profile;
+end
+
+return {
+	profiles = profiles;
+	get_default_version = get_default_version;
+	get_latest_version = get_latest_version;
+	get_default_profile_name = get_default_profile_name;
+	get_valid_versions = get_valid_versions;
+	get_valid_profile_names = get_valid_profile_names;
+	get_profile = get_profile;
+};