Mercurial > prosody-hg
changeset 14218:926f25af2ffe 13.0
util.tlsref: New util library to encapsulate the Mozilla/TLSRef recommendations
Previously we embedded this data directly into core.certmanager. This splits
it out, for various benefits:
- Removes data from the business logic (the config parsing is complex enough
as it is)
- Allows easier testing and tracking of the data (this commit adds various
consistency checks, so that we can have more confidence in future updates
to the data not breaking stuff)
This library also supports multiple versions of the recommendations.
Previously we picked a single version, and put that into certmanager. But this
meant we had to make choices for our users, and one of the choices is between
advancing security standards and ensuring we don't break connectivity for
people doing minor upgrades (deterring people from performing minor upgrades
would have a security impact too).
This library supports the concept of a "default" and a "latest" version, so we
are now free to add new versions into stable releases, and admins can pick
between compatibility and security.
| author | Matthew Wild <mwild1@gmail.com> |
|---|---|
| date | Fri, 12 Jun 2026 13:01:56 +0100 |
| parents | 7eb0e47418ab |
| children | 7f4fcbb1bbe4 |
| files | spec/util_tlsref_spec.lua util/tlsref.lua |
| diffstat | 2 files changed, 400 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/spec/util_tlsref_spec.lua Fri Jun 12 13:01:56 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);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/util/tlsref.lua Fri Jun 12 13:01:56 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; +};
