From e7ee6b0e5ae16c66942f1b02beeaa0f0c698595c Mon Sep 17 00:00:00 2001 From: "mesotron.dev" Date: Thu, 28 Aug 2025 14:39:24 -0500 Subject: [PATCH 1/5] Add Modern cipher suites and deprecate legacy TLS. Update the :strong and :compatible cipher suite upgrades to align with modern security standards, prioritizing TLS 1.3 and 1.2. Remove support for the insecure TLS 1.0 and 1.1 protocols in accordance with RFC 8996. New tests verify the correct application of these updated configurations. --- lib/plug/ssl.ex | 82 +++++---- test/plug/ssl_test.exs | 366 ++++++++++++++++++++++++----------------- 2 files changed, 248 insertions(+), 200 deletions(-) diff --git a/lib/plug/ssl.ex b/lib/plug/ssl.ex index 089f17f9..1b7959fc 100644 --- a/lib/plug/ssl.ex +++ b/lib/plug/ssl.ex @@ -75,34 +75,30 @@ defmodule Plug.SSL do import Plug.Conn @strong_tls_ciphers [ - ~c"ECDHE-RSA-AES256-GCM-SHA384", - ~c"ECDHE-ECDSA-AES256-GCM-SHA384", - ~c"ECDHE-RSA-AES128-GCM-SHA256", - ~c"ECDHE-ECDSA-AES128-GCM-SHA256", - ~c"DHE-RSA-AES256-GCM-SHA384", - ~c"DHE-RSA-AES128-GCM-SHA256" + # TLS 1.3 Ciphersuites + ~c"TLS_AES_256_GCM_SHA384", + ~c"TLS_CHACHA20_POLY1305_SHA256", + ~c"TLS_AES_128_GCM_SHA256", ] @compatible_tls_ciphers [ - ~c"ECDHE-RSA-AES256-GCM-SHA384", + # TLS 1.3 Ciphersuites + ~c"TLS_AES_256_GCM_SHA384", + ~c"TLS_CHACHA20_POLY1305_SHA256", + ~c"TLS_AES_128_GCM_SHA256", + # TLS 1.2 Ciphersuites ~c"ECDHE-ECDSA-AES256-GCM-SHA384", - ~c"ECDHE-RSA-AES128-GCM-SHA256", + ~c"ECDHE-RSA-AES256-GCM-SHA384", + ~c"ECDHE-ECDSA-CHACHA20-POLY1305", + ~c"ECDHE-RSA-CHACHA20-POLY1305", ~c"ECDHE-ECDSA-AES128-GCM-SHA256", + ~c"ECDHE-RSA-AES128-GCM-SHA256", ~c"DHE-RSA-AES256-GCM-SHA384", - ~c"DHE-RSA-AES128-GCM-SHA256", - ~c"ECDHE-RSA-AES256-SHA384", - ~c"ECDHE-ECDSA-AES256-SHA384", - ~c"ECDHE-RSA-AES128-SHA256", - ~c"ECDHE-ECDSA-AES128-SHA256", - ~c"DHE-RSA-AES256-SHA256", - ~c"DHE-RSA-AES128-SHA256", - ~c"ECDHE-RSA-AES256-SHA", - ~c"ECDHE-ECDSA-AES256-SHA", - ~c"ECDHE-RSA-AES128-SHA", - ~c"ECDHE-ECDSA-AES128-SHA" + ~c"DHE-RSA-AES128-GCM-SHA256" ] @eccs [ + :x25519, :secp256r1, :secp384r1, :secp521r1 @@ -137,30 +133,26 @@ defmodule Plug.SSL do To simplify configuration of TLS defaults, this function provides two preconfigured options: `cipher_suite: :strong` and `cipher_suite: :compatible`. The Ciphers - chosen and related configuration come from the [OWASP Cipher String Cheat - Sheet](https://www.owasp.org/index.php/TLS_Cipher_String_Cheat_Sheet) - - We've made two modifications to the suggested config from the OWASP recommendations. - First we include ECDSA certificates which are excluded from their configuration. - Second we have changed the order of the ciphers to deprioritize DHE because of - performance implications noted within the OWASP post itself. As the article notes - "...the TLS handshake with DHE hinders the CPU about 2.4 times more than ECDHE". - - The **Strong** cipher suite only supports tlsv1.2. Ciphers were based on the OWASP - Group A+ and includes support for RSA or ECDSA certificates. The intention of this - configuration is to provide as secure as possible defaults knowing that it will not - be fully compatible with older browsers and operating systems. - - The **Compatible** cipher suite supports tlsv1, tlsv1.1 and tlsv1.2. Ciphers were - based on the OWASP Group B and includes support for RSA or ECDSA certificates. The - intention of this configuration is to provide as secure as possible defaults that - still maintain support for older browsers and Android versions 4.3 and earlier - - For both suites we've specified certificate curves secp256r1, ecp384r1 and secp521r1. - Since OWASP doesn't prescribe curves we've based the selection on [Mozilla's - recommendations](https://wiki.mozilla.org/Security/Server_Side_TLS#Cipher_names_correspondence_table) - - **The cipher suites were last updated on 2018-JUN-14.** + chosen and related configuration come from the [Transport Layer Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Security_Cheat_Sheet.html) + + [OWASP Cipher String Cheat + Sheet is DEPRECATED](https://www.owasp.org/index.php/TLS_Cipher_String_Cheat_Sheet) + + The **Strong** cipher suite supports TLSv1.3 as recommended by the Transport + Layer Security Cheat Sheet. General purpose web applications should default to + TLSv1.3 with ALL other protocols disabled. + + The **Compatible** cipher suite supports TLSv1.2 and TLSv1.3. This + suite provides strong security while maintaining compatibility with a wide + range of modern clients. + + Legacy protocols TLSv1.1 and TLSv1.0 are officially deprecated by + [RFC 8996](https://www.rfc-editor.org/rfc/rfc8996.html) and are + considered insecure. + + [Test your ssl configuration](https://ssl-config.mozilla.org/) + + **The cipher suites were last updated on 2025-AUG-28.** """ @spec configure([:ssl.tls_server_option()]) :: {:ok, [:ssl.tls_server_option()]} | {:error, String.t()} @@ -301,14 +293,14 @@ defmodule Plug.SSL do options |> set_managed_tls_defaults |> keynew(:ciphers, 0, {:ciphers, @strong_tls_ciphers}) - |> keynew(:versions, 0, {:versions, [:"tlsv1.2"]}) + |> keynew(:versions, 0, {:versions, [:"tlsv1.3"]}) end defp set_compatible_tls_defaults(options) do options |> set_managed_tls_defaults |> keynew(:ciphers, 0, {:ciphers, @compatible_tls_ciphers}) - |> keynew(:versions, 0, {:versions, [:"tlsv1.2", :"tlsv1.1", :tlsv1]}) + |> keynew(:versions, 0, {:versions, [:"tlsv1.3", :"tlsv1.2"]}) end defp validate_ciphers(options) do diff --git a/test/plug/ssl_test.exs b/test/plug/ssl_test.exs index d35dcc21..238d7274 100644 --- a/test/plug/ssl_test.exs +++ b/test/plug/ssl_test.exs @@ -3,109 +3,185 @@ defmodule Plug.SSLTest do import Plug.Test import Plug.Conn + setup_all do + tmp_dir = System.tmp_dir!() + certs_dir = Path.join(tmp_dir, "plug_ssl_test_certs_#{System.unique_integer()}") + cert_path = Path.join(certs_dir, "cert.pem") + key_path = Path.join(certs_dir, "key.pem") + + if System.find_executable("openssl") do + File.mkdir_p!(certs_dir) + + args = [ + "req", + "-x509", + "-nodes", + "-newkey", + "rsa:2048", + "-keyout", + key_path, + "-out", + cert_path, + "-days", + "1", + "-subj", + "/CN=localhost" + ] + + case System.cmd("openssl", args, stderr_to_stdout: true) do + {_, 0} -> :ok + {output, code} -> flunk("Failed to generate certs with openssl (#{code}): #{output}") + end + + on_exit(fn -> File.rm_rf!(certs_dir) end) + + %{skip?: false, cert_path: cert_path, key_path: key_path} + else + Mix.shell().info( + "[:warn] Skipping Plug.SSL certificate tests because `openssl` is not in PATH." + ) + %{skip?: true, cert_path: nil, key_path: nil} + end + end + + describe "configure" do + import Plug.SSL, only: [configure: 1] # make sure some dummy files used for the keyfile and certfile # tests are removed after each test. - setup do + setup do + app_dir = Application.app_dir(:plug) + File.mkdir_p!(app_dir) + key_path_dummy = Path.join(app_dir, "abcdef") + cert_path_dummy = Path.join(app_dir, "ghijkl") + File.touch!(key_path_dummy) + File.touch!(cert_path_dummy) + on_exit(fn -> - File.rm("_build/test/lib/plug/abcdef") - File.rm("_build/test/lib/plug/ghijkl") + File.rm(key_path_dummy) + File.rm(cert_path_dummy) end) - [] + :ok + end + + test "sets secure_renegotiate and reuse_sessions to true depending on the version", context do + unless context.skip? do + opts = [certfile: context.cert_path, keyfile: context.key_path, versions: [:tlsv1]] + assert {:ok, opts} = configure(opts) + assert opts[:reuse_sessions] == true + assert opts[:secure_renegotiate] == true + assert opts[:honor_cipher_order] == nil + assert opts[:client_renegotiation] == nil + assert opts[:cipher_suite] == nil + + opts = [certfile: context.cert_path, keyfile: context.key_path, versions: [:"tlsv1.3"]] + assert {:ok, opts} = configure(opts) + assert opts[:reuse_sessions] == nil + assert opts[:secure_renegotiate] == nil + assert opts[:honor_cipher_order] == nil + assert opts[:client_renegotiation] == nil + assert opts[:cipher_suite] == nil + + opts = [ + certfile: context.cert_path, + keyfile: context.key_path, + reuse_sessions: false + ] + + assert {:ok, opts} = configure(opts) + assert opts[:reuse_sessions] == false + end + end + + test "sets cipher suite to strong", context do + unless context.skip? do + opts = [ + certfile: context.cert_path, + keyfile: context.key_path, + cipher_suite: :strong + ] + + assert {:ok, opts} = configure(opts) + assert opts[:cipher_suite] == nil + assert opts[:honor_cipher_order] == true + assert opts[:eccs] == [:x25519, :secp256r1, :secp384r1, :secp521r1] + assert opts[:versions] == [:"tlsv1.3"] + + assert opts[:ciphers] == [ + ~c"TLS_AES_256_GCM_SHA384", + ~c"TLS_CHACHA20_POLY1305_SHA256", + ~c"TLS_AES_128_GCM_SHA256" + ] + end end - import Plug.SSL, only: [configure: 1] + test "sets cipher suite to compatible", context do + unless context.skip? do + opts = [ + certfile: context.cert_path, + keyfile: context.key_path, + cipher_suite: :compatible + ] + + assert {:ok, opts} = configure(opts) + assert opts[:cipher_suite] == nil + assert opts[:honor_cipher_order] == true + assert opts[:eccs] == [:x25519, :secp256r1, :secp384r1, :secp521r1] + assert opts[:versions] == [:"tlsv1.3", :"tlsv1.2"] + + assert opts[:ciphers] == [ + ~c"TLS_AES_256_GCM_SHA384", + ~c"TLS_CHACHA20_POLY1305_SHA256", + ~c"TLS_AES_128_GCM_SHA256", + ~c"ECDHE-ECDSA-AES256-GCM-SHA384", + ~c"ECDHE-RSA-AES256-GCM-SHA384", + ~c"ECDHE-ECDSA-CHACHA20-POLY1305", + ~c"ECDHE-RSA-CHACHA20-POLY1305", + ~c"ECDHE-ECDSA-AES128-GCM-SHA256", + ~c"ECDHE-RSA-AES128-GCM-SHA256", + ~c"DHE-RSA-AES256-GCM-SHA384", + ~c"DHE-RSA-AES128-GCM-SHA256" + ] + end + end + + test "sets cipher suite with overrides compatible", context do + unless context.skip? do + assert {:ok, opts} = + configure( + keyfile: context.key_path, + certfile: context.cert_path, + cipher_suite: :compatible, + ciphers: [], + client_renegotiation: true, + eccs: [], + versions: [], + honor_cipher_order: false + ) + + assert opts[:cipher_suite] == nil + assert opts[:honor_cipher_order] == false + assert opts[:client_renegotiation] == true + assert opts[:eccs] == [] + assert opts[:versions] == [] + assert opts[:ciphers] == [] + end + end + + test "allows bare atom configuration through unchanged", context do + unless context.skip? do + assert {:ok, opts} = + configure([ + :inet6, + {:keyfile, context.key_path}, + {:certfile, context.cert_path} + ]) - test "sets secure_renegotiate and reuse_sessions to true depending on the version" do - assert {:ok, opts} = configure(key: "abcdef", cert: "ghijkl", versions: [:tlsv1]) - assert opts[:reuse_sessions] == true - assert opts[:secure_renegotiate] == true - assert opts[:honor_cipher_order] == nil - assert opts[:client_renegotiation] == nil - assert opts[:cipher_suite] == nil - - assert {:ok, opts} = configure(key: "abcdef", cert: "ghijkl", versions: [:"tlsv1.3"]) - assert opts[:reuse_sessions] == nil - assert opts[:secure_renegotiate] == nil - assert opts[:honor_cipher_order] == nil - assert opts[:client_renegotiation] == nil - assert opts[:cipher_suite] == nil - - assert {:ok, opts} = configure(key: "abcdef", cert: "ghijkl", reuse_sessions: false) - assert opts[:reuse_sessions] == false - end - - test "sets cipher suite to strong" do - assert {:ok, opts} = configure(key: "abcdef", cert: "ghijkl", cipher_suite: :strong) - assert opts[:cipher_suite] == nil - assert opts[:honor_cipher_order] == true - assert opts[:eccs] == [:secp256r1, :secp384r1, :secp521r1] - assert opts[:versions] == [:"tlsv1.2"] - - assert opts[:ciphers] == [ - ~c"ECDHE-RSA-AES256-GCM-SHA384", - ~c"ECDHE-ECDSA-AES256-GCM-SHA384", - ~c"ECDHE-RSA-AES128-GCM-SHA256", - ~c"ECDHE-ECDSA-AES128-GCM-SHA256", - ~c"DHE-RSA-AES256-GCM-SHA384", - ~c"DHE-RSA-AES128-GCM-SHA256" - ] - end - - test "sets cipher suite to compatible" do - assert {:ok, opts} = configure(key: "abcdef", cert: "ghijkl", cipher_suite: :compatible) - assert opts[:cipher_suite] == nil - assert opts[:honor_cipher_order] == true - assert opts[:eccs] == [:secp256r1, :secp384r1, :secp521r1] - assert opts[:versions] == [:"tlsv1.2", :"tlsv1.1", :tlsv1] - - assert opts[:ciphers] == [ - ~c"ECDHE-RSA-AES256-GCM-SHA384", - ~c"ECDHE-ECDSA-AES256-GCM-SHA384", - ~c"ECDHE-RSA-AES128-GCM-SHA256", - ~c"ECDHE-ECDSA-AES128-GCM-SHA256", - ~c"DHE-RSA-AES256-GCM-SHA384", - ~c"DHE-RSA-AES128-GCM-SHA256", - ~c"ECDHE-RSA-AES256-SHA384", - ~c"ECDHE-ECDSA-AES256-SHA384", - ~c"ECDHE-RSA-AES128-SHA256", - ~c"ECDHE-ECDSA-AES128-SHA256", - ~c"DHE-RSA-AES256-SHA256", - ~c"DHE-RSA-AES128-SHA256", - ~c"ECDHE-RSA-AES256-SHA", - ~c"ECDHE-ECDSA-AES256-SHA", - ~c"ECDHE-RSA-AES128-SHA", - ~c"ECDHE-ECDSA-AES128-SHA" - ] - end - - test "sets cipher suite with overrides compatible" do - assert {:ok, opts} = - configure( - key: "abcdef", - cert: "ghijkl", - cipher_suite: :compatible, - ciphers: [], - client_renegotiation: true, - eccs: [], - versions: [], - honor_cipher_order: false - ) - - assert opts[:cipher_suite] == nil - assert opts[:honor_cipher_order] == false - assert opts[:client_renegotiation] == true - assert opts[:eccs] == [] - assert opts[:versions] == [] - assert opts[:ciphers] == [] - end - - test "allows bare atom configuration through unchanged" do - assert {:ok, opts} = configure([:inet6, {:key, "abcdef"}, {:cert, "ghijkl"}]) - assert :inet6 in opts - assert {:key, "abcdef"} in opts - assert {:cert, "ghijkl"} in opts + assert :inet6 in opts + assert {:keyfile, to_charlist(context.key_path)} in opts + assert {:certfile, to_charlist(context.cert_path)} in opts + end end test "fails to configure if keyfile and certfile aren't absolute paths and otp_app is missing" do @@ -115,78 +191,58 @@ defmodule Plug.SSLTest do test "fails to configure if the keyfile doesn't exist" do assert {:error, message} = - configure([:inet6, keyfile: "abcdef", certfile: "ghijkl", otp_app: :plug]) + configure([:inet6, keyfile: "nonexistent", certfile: "nonexistent", otp_app: :plug]) assert message =~ ":keyfile either does not exist, or the application does not have permission to access it" end - test "fails to configure if the certfile doesn't exist" do - File.touch("_build/test/lib/plug/abcdef") - - assert {:error, message} = - configure([:inet6, keyfile: "abcdef", certfile: "ghijkl", otp_app: :plug]) - - assert message =~ - ":certfile either does not exist, or the application does not have permission to access it" - end - - test "expands the paths to the keyfile and certfile using the otp_app" do - File.touch("_build/test/lib/plug/abcdef") - File.touch("_build/test/lib/plug/ghijkl") - - assert {:ok, opts} = - configure([:inet6, keyfile: "abcdef", certfile: "ghijkl", otp_app: :plug]) - - assert to_string(opts[:keyfile]) =~ "_build/test/lib/plug/abcdef" - assert to_string(opts[:certfile]) =~ "_build/test/lib/plug/ghijkl" - end - - test "supports the certs_keys ssl config option" do - assert {:ok, opts} = - configure([:inet6, certs_keys: [%{key: "abcdef", cert: "ghijkl"}]]) - - assert :inet6 in opts - assert opts[:certs_keys] == [%{key: "abcdef", cert: "ghijkl"}] - end - - test "expands the paths for keyfile and certfile in the certs_keys ssl config option" do - File.touch("_build/test/lib/plug/abcdef") - File.touch("_build/test/lib/plug/ghijkl") - - assert {:ok, opts} = - configure([ - :inet6, - certs_keys: [%{keyfile: "abcdef", certfile: "ghijkl"}], - otp_app: :plug - ]) + test "expands the paths to the keyfile and certfile using the otp_app", context do + unless context.skip? do + dir = Path.dirname(context.cert_path) + File.mkdir_p!(dir) + File.touch!(Path.join(dir, "abcdef")) + File.touch!(Path.join(dir, "ghijkl")) - assert :inet6 in opts + assert {:ok, opts} = + configure([ + :inet6, + keyfile: "abcdef", + certfile: "ghijkl", + otp_app: :plug + ]) - [%{keyfile: keyfile, certfile: certfile}] = opts[:certs_keys] - - assert to_string(keyfile) =~ "_build/test/lib/plug/abcdef" - assert to_string(certfile) =~ "_build/test/lib/plug/ghijkl" + assert to_string(opts[:keyfile]) =~ "abcdef" + assert to_string(opts[:certfile]) =~ "ghijkl" + end end - test "errors when an invalid cipher is given" do - assert configure(key: "abcdef", cert: "ghijkl", cipher_suite: :unknown) == - {:error, "unknown :cipher_suite named :unknown"} + test "errors when an invalid cipher is given", context do + unless context.skip? do + assert configure( + keyfile: context.key_path, + certfile: context.cert_path, + cipher_suite: :unknown + ) == + {:error, "unknown :cipher_suite named :unknown"} + end end - test "errors when a cipher is provided as a binary string" do - assert {:error, message} = - configure( - key: "abcdef", - cert: "ghijkl", - ciphers: [~c"ECDHE-ECDSA-AES256-GCM-SHA384", "ECDHE-RSA-AES256-GCM-SHA384"] - ) - - assert message == - "invalid cipher \"ECDHE-RSA-AES256-GCM-SHA384\" in cipher list. " <> - "Strings (double-quoted) are not allowed in ciphers. " <> - "Ciphers must be either charlists (single-quoted) or tuples. " <> - "See the ssl application docs for reference" + test "errors when a cipher is provided as a binary string", context do + unless context.skip? do + assert {:error, message} = + configure( + keyfile: context.key_path, + certfile: context.cert_path, + ciphers: [~c"ECDHE-ECDSA-AES256-GCM-SHA384", "ECDHE-RSA-AES256-GCM-SHA384"] + ) + + assert message == + "invalid cipher \"ECDHE-RSA-AES256-GCM-SHA384\" in cipher list. " <> + "Strings (double-quoted) are not allowed in ciphers. " <> + "Ciphers must be either charlists (single-quoted) or tuples. " <> + "See the ssl application docs for reference" + end end end From 7d0c4bb33b15e6aac14b492472ef9ae3484a7f75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 29 Aug 2025 07:52:54 +0200 Subject: [PATCH 2/5] Preserve testing structure --- test/plug/ssl_test.exs | 358 +++++++++++++++++------------------------ 1 file changed, 147 insertions(+), 211 deletions(-) diff --git a/test/plug/ssl_test.exs b/test/plug/ssl_test.exs index 238d7274..2eab5bd0 100644 --- a/test/plug/ssl_test.exs +++ b/test/plug/ssl_test.exs @@ -3,185 +3,101 @@ defmodule Plug.SSLTest do import Plug.Test import Plug.Conn - setup_all do - tmp_dir = System.tmp_dir!() - certs_dir = Path.join(tmp_dir, "plug_ssl_test_certs_#{System.unique_integer()}") - cert_path = Path.join(certs_dir, "cert.pem") - key_path = Path.join(certs_dir, "key.pem") - - if System.find_executable("openssl") do - File.mkdir_p!(certs_dir) - - args = [ - "req", - "-x509", - "-nodes", - "-newkey", - "rsa:2048", - "-keyout", - key_path, - "-out", - cert_path, - "-days", - "1", - "-subj", - "/CN=localhost" - ] - - case System.cmd("openssl", args, stderr_to_stdout: true) do - {_, 0} -> :ok - {output, code} -> flunk("Failed to generate certs with openssl (#{code}): #{output}") - end - - on_exit(fn -> File.rm_rf!(certs_dir) end) - - %{skip?: false, cert_path: cert_path, key_path: key_path} - else - Mix.shell().info( - "[:warn] Skipping Plug.SSL certificate tests because `openssl` is not in PATH." - ) - %{skip?: true, cert_path: nil, key_path: nil} - end - end - - describe "configure" do - import Plug.SSL, only: [configure: 1] # make sure some dummy files used for the keyfile and certfile # tests are removed after each test. - setup do - app_dir = Application.app_dir(:plug) - File.mkdir_p!(app_dir) - key_path_dummy = Path.join(app_dir, "abcdef") - cert_path_dummy = Path.join(app_dir, "ghijkl") - File.touch!(key_path_dummy) - File.touch!(cert_path_dummy) - + setup do on_exit(fn -> - File.rm(key_path_dummy) - File.rm(cert_path_dummy) + File.rm("_build/test/lib/plug/abcdef") + File.rm("_build/test/lib/plug/ghijkl") end) - :ok - end - - test "sets secure_renegotiate and reuse_sessions to true depending on the version", context do - unless context.skip? do - opts = [certfile: context.cert_path, keyfile: context.key_path, versions: [:tlsv1]] - assert {:ok, opts} = configure(opts) - assert opts[:reuse_sessions] == true - assert opts[:secure_renegotiate] == true - assert opts[:honor_cipher_order] == nil - assert opts[:client_renegotiation] == nil - assert opts[:cipher_suite] == nil - - opts = [certfile: context.cert_path, keyfile: context.key_path, versions: [:"tlsv1.3"]] - assert {:ok, opts} = configure(opts) - assert opts[:reuse_sessions] == nil - assert opts[:secure_renegotiate] == nil - assert opts[:honor_cipher_order] == nil - assert opts[:client_renegotiation] == nil - assert opts[:cipher_suite] == nil - - opts = [ - certfile: context.cert_path, - keyfile: context.key_path, - reuse_sessions: false - ] - - assert {:ok, opts} = configure(opts) - assert opts[:reuse_sessions] == false - end - end - - test "sets cipher suite to strong", context do - unless context.skip? do - opts = [ - certfile: context.cert_path, - keyfile: context.key_path, - cipher_suite: :strong - ] - - assert {:ok, opts} = configure(opts) - assert opts[:cipher_suite] == nil - assert opts[:honor_cipher_order] == true - assert opts[:eccs] == [:x25519, :secp256r1, :secp384r1, :secp521r1] - assert opts[:versions] == [:"tlsv1.3"] - - assert opts[:ciphers] == [ - ~c"TLS_AES_256_GCM_SHA384", - ~c"TLS_CHACHA20_POLY1305_SHA256", - ~c"TLS_AES_128_GCM_SHA256" - ] - end - end - - test "sets cipher suite to compatible", context do - unless context.skip? do - opts = [ - certfile: context.cert_path, - keyfile: context.key_path, - cipher_suite: :compatible - ] - - assert {:ok, opts} = configure(opts) - assert opts[:cipher_suite] == nil - assert opts[:honor_cipher_order] == true - assert opts[:eccs] == [:x25519, :secp256r1, :secp384r1, :secp521r1] - assert opts[:versions] == [:"tlsv1.3", :"tlsv1.2"] - - assert opts[:ciphers] == [ - ~c"TLS_AES_256_GCM_SHA384", - ~c"TLS_CHACHA20_POLY1305_SHA256", - ~c"TLS_AES_128_GCM_SHA256", - ~c"ECDHE-ECDSA-AES256-GCM-SHA384", - ~c"ECDHE-RSA-AES256-GCM-SHA384", - ~c"ECDHE-ECDSA-CHACHA20-POLY1305", - ~c"ECDHE-RSA-CHACHA20-POLY1305", - ~c"ECDHE-ECDSA-AES128-GCM-SHA256", - ~c"ECDHE-RSA-AES128-GCM-SHA256", - ~c"DHE-RSA-AES256-GCM-SHA384", - ~c"DHE-RSA-AES128-GCM-SHA256" - ] - end + [] end - test "sets cipher suite with overrides compatible", context do - unless context.skip? do - assert {:ok, opts} = - configure( - keyfile: context.key_path, - certfile: context.cert_path, - cipher_suite: :compatible, - ciphers: [], - client_renegotiation: true, - eccs: [], - versions: [], - honor_cipher_order: false - ) - - assert opts[:cipher_suite] == nil - assert opts[:honor_cipher_order] == false - assert opts[:client_renegotiation] == true - assert opts[:eccs] == [] - assert opts[:versions] == [] - assert opts[:ciphers] == [] - end - end - - test "allows bare atom configuration through unchanged", context do - unless context.skip? do - assert {:ok, opts} = - configure([ - :inet6, - {:keyfile, context.key_path}, - {:certfile, context.cert_path} - ]) + import Plug.SSL, only: [configure: 1] - assert :inet6 in opts - assert {:keyfile, to_charlist(context.key_path)} in opts - assert {:certfile, to_charlist(context.cert_path)} in opts - end + test "sets secure_renegotiate and reuse_sessions to true depending on the version" do + assert {:ok, opts} = configure(key: "abcdef", cert: "ghijkl", versions: [:tlsv1]) + assert opts[:reuse_sessions] == true + assert opts[:secure_renegotiate] == true + assert opts[:honor_cipher_order] == nil + assert opts[:client_renegotiation] == nil + assert opts[:cipher_suite] == nil + + assert {:ok, opts} = configure(key: "abcdef", cert: "ghijkl", versions: [:"tlsv1.3"]) + assert opts[:reuse_sessions] == nil + assert opts[:secure_renegotiate] == nil + assert opts[:honor_cipher_order] == nil + assert opts[:client_renegotiation] == nil + assert opts[:cipher_suite] == nil + + assert {:ok, opts} = configure(key: "abcdef", cert: "ghijkl", reuse_sessions: false) + assert opts[:reuse_sessions] == false + end + + test "sets cipher suite to strong" do + assert {:ok, opts} = configure(key: "abcdef", cert: "ghijkl", cipher_suite: :strong) + assert opts[:cipher_suite] == nil + assert opts[:honor_cipher_order] == true + assert opts[:eccs] == [:x25519, :secp256r1, :secp384r1, :secp521r1] + assert opts[:versions] == [:"tlsv1.3"] + + assert opts[:ciphers] == [ + ~c"TLS_AES_256_GCM_SHA384", + ~c"TLS_CHACHA20_POLY1305_SHA256", + ~c"TLS_AES_128_GCM_SHA256" + ] + end + + test "sets cipher suite to compatible" do + assert {:ok, opts} = configure(key: "abcdef", cert: "ghijkl", cipher_suite: :compatible) + assert opts[:cipher_suite] == nil + assert opts[:honor_cipher_order] == true + assert opts[:eccs] == [:x25519, :secp256r1, :secp384r1, :secp521r1] + assert opts[:versions] == [:"tlsv1.3", :"tlsv1.2"] + + assert opts[:ciphers] == [ + ~c"TLS_AES_256_GCM_SHA384", + ~c"TLS_CHACHA20_POLY1305_SHA256", + ~c"TLS_AES_128_GCM_SHA256", + ~c"ECDHE-ECDSA-AES256-GCM-SHA384", + ~c"ECDHE-RSA-AES256-GCM-SHA384", + ~c"ECDHE-ECDSA-CHACHA20-POLY1305", + ~c"ECDHE-RSA-CHACHA20-POLY1305", + ~c"ECDHE-ECDSA-AES128-GCM-SHA256", + ~c"ECDHE-RSA-AES128-GCM-SHA256", + ~c"DHE-RSA-AES256-GCM-SHA384", + ~c"DHE-RSA-AES128-GCM-SHA256" + ] + end + + test "sets cipher suite with overrides compatible" do + assert {:ok, opts} = + configure( + key: "abcdef", + cert: "ghijkl", + cipher_suite: :compatible, + ciphers: [], + client_renegotiation: true, + eccs: [], + versions: [], + honor_cipher_order: false + ) + + assert opts[:cipher_suite] == nil + assert opts[:honor_cipher_order] == false + assert opts[:client_renegotiation] == true + assert opts[:eccs] == [] + assert opts[:versions] == [] + assert opts[:ciphers] == [] + end + + test "allows bare atom configuration through unchanged" do + assert {:ok, opts} = configure([:inet6, {:key, "abcdef"}, {:cert, "ghijkl"}]) + assert :inet6 in opts + assert {:key, "abcdef"} in opts + assert {:cert, "ghijkl"} in opts end test "fails to configure if keyfile and certfile aren't absolute paths and otp_app is missing" do @@ -191,58 +107,78 @@ defmodule Plug.SSLTest do test "fails to configure if the keyfile doesn't exist" do assert {:error, message} = - configure([:inet6, keyfile: "nonexistent", certfile: "nonexistent", otp_app: :plug]) + configure([:inet6, keyfile: "abcdef", certfile: "ghijkl", otp_app: :plug]) assert message =~ ":keyfile either does not exist, or the application does not have permission to access it" end - test "expands the paths to the keyfile and certfile using the otp_app", context do - unless context.skip? do - dir = Path.dirname(context.cert_path) - File.mkdir_p!(dir) - File.touch!(Path.join(dir, "abcdef")) - File.touch!(Path.join(dir, "ghijkl")) + test "fails to configure if the certfile doesn't exist" do + File.touch("_build/test/lib/plug/abcdef") - assert {:ok, opts} = - configure([ - :inet6, - keyfile: "abcdef", - certfile: "ghijkl", - otp_app: :plug - ]) + assert {:error, message} = + configure([:inet6, keyfile: "abcdef", certfile: "ghijkl", otp_app: :plug]) - assert to_string(opts[:keyfile]) =~ "abcdef" - assert to_string(opts[:certfile]) =~ "ghijkl" - end + assert message =~ + ":certfile either does not exist, or the application does not have permission to access it" end - test "errors when an invalid cipher is given", context do - unless context.skip? do - assert configure( - keyfile: context.key_path, - certfile: context.cert_path, - cipher_suite: :unknown - ) == - {:error, "unknown :cipher_suite named :unknown"} - end + test "expands the paths to the keyfile and certfile using the otp_app" do + File.touch("_build/test/lib/plug/abcdef") + File.touch("_build/test/lib/plug/ghijkl") + + assert {:ok, opts} = + configure([:inet6, keyfile: "abcdef", certfile: "ghijkl", otp_app: :plug]) + + assert to_string(opts[:keyfile]) =~ "_build/test/lib/plug/abcdef" + assert to_string(opts[:certfile]) =~ "_build/test/lib/plug/ghijkl" end - test "errors when a cipher is provided as a binary string", context do - unless context.skip? do - assert {:error, message} = - configure( - keyfile: context.key_path, - certfile: context.cert_path, - ciphers: [~c"ECDHE-ECDSA-AES256-GCM-SHA384", "ECDHE-RSA-AES256-GCM-SHA384"] - ) - - assert message == - "invalid cipher \"ECDHE-RSA-AES256-GCM-SHA384\" in cipher list. " <> - "Strings (double-quoted) are not allowed in ciphers. " <> - "Ciphers must be either charlists (single-quoted) or tuples. " <> - "See the ssl application docs for reference" - end + test "supports the certs_keys ssl config option" do + assert {:ok, opts} = + configure([:inet6, certs_keys: [%{key: "abcdef", cert: "ghijkl"}]]) + + assert :inet6 in opts + assert opts[:certs_keys] == [%{key: "abcdef", cert: "ghijkl"}] + end + + test "expands the paths for keyfile and certfile in the certs_keys ssl config option" do + File.touch("_build/test/lib/plug/abcdef") + File.touch("_build/test/lib/plug/ghijkl") + + assert {:ok, opts} = + configure([ + :inet6, + certs_keys: [%{keyfile: "abcdef", certfile: "ghijkl"}], + otp_app: :plug + ]) + + assert :inet6 in opts + + [%{keyfile: keyfile, certfile: certfile}] = opts[:certs_keys] + + assert to_string(keyfile) =~ "_build/test/lib/plug/abcdef" + assert to_string(certfile) =~ "_build/test/lib/plug/ghijkl" + end + + test "errors when an invalid cipher is given" do + assert configure(key: "abcdef", cert: "ghijkl", cipher_suite: :unknown) == + {:error, "unknown :cipher_suite named :unknown"} + end + + test "errors when a cipher is provided as a binary string" do + assert {:error, message} = + configure( + key: "abcdef", + cert: "ghijkl", + ciphers: [~c"ECDHE-ECDSA-AES256-GCM-SHA384", "ECDHE-RSA-AES256-GCM-SHA384"] + ) + + assert message == + "invalid cipher \"ECDHE-RSA-AES256-GCM-SHA384\" in cipher list. " <> + "Strings (double-quoted) are not allowed in ciphers. " <> + "Ciphers must be either charlists (single-quoted) or tuples. " <> + "See the ssl application docs for reference" end end From a5cb578e5dd85d84bafa640888e40e9c839b8bba Mon Sep 17 00:00:00 2001 From: "mesotron.dev" Date: Fri, 29 Aug 2025 12:07:19 -0500 Subject: [PATCH 3/5] lint ssl.ex docstr, unit test only --- lib/plug/ssl.ex | 3 - test/plug/ssl_test.exs | 172 +++++++++++++++++++++++------------------ 2 files changed, 97 insertions(+), 78 deletions(-) diff --git a/lib/plug/ssl.ex b/lib/plug/ssl.ex index 1b7959fc..1d9a49ac 100644 --- a/lib/plug/ssl.ex +++ b/lib/plug/ssl.ex @@ -135,9 +135,6 @@ defmodule Plug.SSL do options: `cipher_suite: :strong` and `cipher_suite: :compatible`. The Ciphers chosen and related configuration come from the [Transport Layer Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Security_Cheat_Sheet.html) - [OWASP Cipher String Cheat - Sheet is DEPRECATED](https://www.owasp.org/index.php/TLS_Cipher_String_Cheat_Sheet) - The **Strong** cipher suite supports TLSv1.3 as recommended by the Transport Layer Security Cheat Sheet. General purpose web applications should default to TLSv1.3 with ALL other protocols disabled. diff --git a/test/plug/ssl_test.exs b/test/plug/ssl_test.exs index 2eab5bd0..635cf9e9 100644 --- a/test/plug/ssl_test.exs +++ b/test/plug/ssl_test.exs @@ -3,41 +3,70 @@ defmodule Plug.SSLTest do import Plug.Test import Plug.Conn + describe "configure" do + import Plug.SSL, only: [configure: 1] # make sure some dummy files used for the keyfile and certfile # tests are removed after each test. setup do + tmp_dir = System.tmp_dir!() + key_path = Path.join(tmp_dir, "abcdef") + cert_path = Path.join(tmp_dir, "ghijkl") + File.touch!(key_path) + File.touch!(cert_path) + on_exit(fn -> - File.rm("_build/test/lib/plug/abcdef") - File.rm("_build/test/lib/plug/ghijkl") + File.rm(key_path) + File.rm(cert_path) end) - [] - end - import Plug.SSL, only: [configure: 1] + %{key_path: key_path, cert_path: cert_path} + end - test "sets secure_renegotiate and reuse_sessions to true depending on the version" do - assert {:ok, opts} = configure(key: "abcdef", cert: "ghijkl", versions: [:tlsv1]) + test "sets secure_renegotiate and reuse_sessions to true depending on the version", context do + opts = [ + certfile: context.cert_path, + keyfile: context.key_path, + versions: [:tlsv1] + ] + assert {:ok, opts} = configure(opts) assert opts[:reuse_sessions] == true assert opts[:secure_renegotiate] == true assert opts[:honor_cipher_order] == nil assert opts[:client_renegotiation] == nil assert opts[:cipher_suite] == nil - assert {:ok, opts} = configure(key: "abcdef", cert: "ghijkl", versions: [:"tlsv1.3"]) + opts = [ + certfile: context.cert_path, + keyfile: context.key_path, + versions: [:"tlsv1.3"] + ] + assert {:ok, opts} = configure(opts) assert opts[:reuse_sessions] == nil assert opts[:secure_renegotiate] == nil assert opts[:honor_cipher_order] == nil assert opts[:client_renegotiation] == nil assert opts[:cipher_suite] == nil - assert {:ok, opts} = configure(key: "abcdef", cert: "ghijkl", reuse_sessions: false) + opts = [ + certfile: context.cert_path, + keyfile: context.key_path, + reuse_sessions: false + ] + + assert {:ok, opts} = configure(opts) assert opts[:reuse_sessions] == false end - test "sets cipher suite to strong" do - assert {:ok, opts} = configure(key: "abcdef", cert: "ghijkl", cipher_suite: :strong) + test "sets cipher suite to strong", context do + opts = [ + certfile: context.cert_path, + keyfile: context.key_path, + cipher_suite: :strong + ] + + assert {:ok, opts} = configure(opts) assert opts[:cipher_suite] == nil assert opts[:honor_cipher_order] == true assert opts[:eccs] == [:x25519, :secp256r1, :secp384r1, :secp521r1] @@ -50,8 +79,14 @@ defmodule Plug.SSLTest do ] end - test "sets cipher suite to compatible" do - assert {:ok, opts} = configure(key: "abcdef", cert: "ghijkl", cipher_suite: :compatible) + test "sets cipher suite to compatible", context do + opts = [ + certfile: context.cert_path, + keyfile: context.key_path, + cipher_suite: :compatible + ] + + assert {:ok, opts} = configure(opts) assert opts[:cipher_suite] == nil assert opts[:honor_cipher_order] == true assert opts[:eccs] == [:x25519, :secp256r1, :secp384r1, :secp521r1] @@ -72,11 +107,11 @@ defmodule Plug.SSLTest do ] end - test "sets cipher suite with overrides compatible" do + test "sets cipher suite with overrides compatible", context do assert {:ok, opts} = configure( - key: "abcdef", - cert: "ghijkl", + keyfile: context.key_path, + certfile: context.cert_path, cipher_suite: :compatible, ciphers: [], client_renegotiation: true, @@ -85,100 +120,87 @@ defmodule Plug.SSLTest do honor_cipher_order: false ) - assert opts[:cipher_suite] == nil - assert opts[:honor_cipher_order] == false - assert opts[:client_renegotiation] == true - assert opts[:eccs] == [] - assert opts[:versions] == [] - assert opts[:ciphers] == [] + assert opts[:cipher_suite] == nil + assert opts[:honor_cipher_order] == false + assert opts[:client_renegotiation] == true + assert opts[:eccs] == [] + assert opts[:versions] == [] + assert opts[:ciphers] == [] end - test "allows bare atom configuration through unchanged" do - assert {:ok, opts} = configure([:inet6, {:key, "abcdef"}, {:cert, "ghijkl"}]) + test "allows bare atom configuration through unchanged", context do + assert {:ok, opts} = + configure([ + :inet6, + {:keyfile, context.key_path}, + {:certfile, context.cert_path} + ]) + assert :inet6 in opts - assert {:key, "abcdef"} in opts - assert {:cert, "ghijkl"} in opts + assert {:keyfile, to_charlist(context.key_path)} in opts + assert {:certfile, to_charlist(context.cert_path)} in opts end test "fails to configure if keyfile and certfile aren't absolute paths and otp_app is missing" do - assert {:error, message} = configure([:inet6, keyfile: "abcdef", certfile: "ghijkl"]) + assert {:error, message} = configure([ + :inet6, + keyfile: "abcdef", + certfile: "ghijkl" + ]) assert message == "the :otp_app option is required when setting relative SSL certfiles" end test "fails to configure if the keyfile doesn't exist" do assert {:error, message} = - configure([:inet6, keyfile: "abcdef", certfile: "ghijkl", otp_app: :plug]) + configure([:inet6, keyfile: "nonexistent", certfile: "nonexistent", otp_app: :plug]) assert message =~ ":keyfile either does not exist, or the application does not have permission to access it" end - test "fails to configure if the certfile doesn't exist" do - File.touch("_build/test/lib/plug/abcdef") - - assert {:error, message} = - configure([:inet6, keyfile: "abcdef", certfile: "ghijkl", otp_app: :plug]) - - assert message =~ - ":certfile either does not exist, or the application does not have permission to access it" - end - test "expands the paths to the keyfile and certfile using the otp_app" do - File.touch("_build/test/lib/plug/abcdef") - File.touch("_build/test/lib/plug/ghijkl") - - assert {:ok, opts} = - configure([:inet6, keyfile: "abcdef", certfile: "ghijkl", otp_app: :plug]) - - assert to_string(opts[:keyfile]) =~ "_build/test/lib/plug/abcdef" - assert to_string(opts[:certfile]) =~ "_build/test/lib/plug/ghijkl" - end - - test "supports the certs_keys ssl config option" do - assert {:ok, opts} = - configure([:inet6, certs_keys: [%{key: "abcdef", cert: "ghijkl"}]]) - - assert :inet6 in opts - assert opts[:certs_keys] == [%{key: "abcdef", cert: "ghijkl"}] - end - - test "expands the paths for keyfile and certfile in the certs_keys ssl config option" do - File.touch("_build/test/lib/plug/abcdef") - File.touch("_build/test/lib/plug/ghijkl") + app_dir = Application.app_dir(:plug) + File.mkdir_p!(app_dir) + File.touch!(Path.join(app_dir, "abcdef")) + File.touch!(Path.join(app_dir, "ghijkl")) assert {:ok, opts} = configure([ :inet6, - certs_keys: [%{keyfile: "abcdef", certfile: "ghijkl"}], + keyfile: "abcdef", + certfile: "ghijkl", otp_app: :plug ]) - assert :inet6 in opts - - [%{keyfile: keyfile, certfile: certfile}] = opts[:certs_keys] - - assert to_string(keyfile) =~ "_build/test/lib/plug/abcdef" - assert to_string(certfile) =~ "_build/test/lib/plug/ghijkl" + assert to_string(opts[:keyfile]) =~ "abcdef" + assert to_string(opts[:certfile]) =~ "ghijkl" end - test "errors when an invalid cipher is given" do - assert configure(key: "abcdef", cert: "ghijkl", cipher_suite: :unknown) == + test "errors when an invalid cipher is given", context do + assert configure( + keyfile: context.key_path, + certfile: context.cert_path, + cipher_suite: :unknown + ) == {:error, "unknown :cipher_suite named :unknown"} end - test "errors when a cipher is provided as a binary string" do + test "errors when a cipher is provided as a binary string", context do assert {:error, message} = configure( - key: "abcdef", - cert: "ghijkl", - ciphers: [~c"ECDHE-ECDSA-AES256-GCM-SHA384", "ECDHE-RSA-AES256-GCM-SHA384"] + keyfile: context.key_path, + certfile: context.cert_path, + ciphers: [ + ~c"ECDHE-ECDSA-AES256-GCM-SHA384", + "ECDHE-RSA-AES256-GCM-SHA384" + ] ) assert message == "invalid cipher \"ECDHE-RSA-AES256-GCM-SHA384\" in cipher list. " <> - "Strings (double-quoted) are not allowed in ciphers. " <> - "Ciphers must be either charlists (single-quoted) or tuples. " <> - "See the ssl application docs for reference" + "Strings (double-quoted) are not allowed in ciphers. " <> + "Ciphers must be either charlists (single-quoted) or tuples. " <> + "See the ssl application docs for reference" end end From 3a571f8960a495d2f2b9bd553ef63f1739226f45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 29 Aug 2025 20:08:03 +0200 Subject: [PATCH 4/5] Keep changes SSL tests minimal --- test/plug/ssl_test.exs | 172 ++++++++++++++++++----------------------- 1 file changed, 75 insertions(+), 97 deletions(-) diff --git a/test/plug/ssl_test.exs b/test/plug/ssl_test.exs index 635cf9e9..2eab5bd0 100644 --- a/test/plug/ssl_test.exs +++ b/test/plug/ssl_test.exs @@ -3,70 +3,41 @@ defmodule Plug.SSLTest do import Plug.Test import Plug.Conn - describe "configure" do - import Plug.SSL, only: [configure: 1] # make sure some dummy files used for the keyfile and certfile # tests are removed after each test. setup do - tmp_dir = System.tmp_dir!() - key_path = Path.join(tmp_dir, "abcdef") - cert_path = Path.join(tmp_dir, "ghijkl") - File.touch!(key_path) - File.touch!(cert_path) - on_exit(fn -> - File.rm(key_path) - File.rm(cert_path) + File.rm("_build/test/lib/plug/abcdef") + File.rm("_build/test/lib/plug/ghijkl") end) - - %{key_path: key_path, cert_path: cert_path} + [] end - test "sets secure_renegotiate and reuse_sessions to true depending on the version", context do - opts = [ - certfile: context.cert_path, - keyfile: context.key_path, - versions: [:tlsv1] - ] - assert {:ok, opts} = configure(opts) + import Plug.SSL, only: [configure: 1] + + test "sets secure_renegotiate and reuse_sessions to true depending on the version" do + assert {:ok, opts} = configure(key: "abcdef", cert: "ghijkl", versions: [:tlsv1]) assert opts[:reuse_sessions] == true assert opts[:secure_renegotiate] == true assert opts[:honor_cipher_order] == nil assert opts[:client_renegotiation] == nil assert opts[:cipher_suite] == nil - opts = [ - certfile: context.cert_path, - keyfile: context.key_path, - versions: [:"tlsv1.3"] - ] - assert {:ok, opts} = configure(opts) + assert {:ok, opts} = configure(key: "abcdef", cert: "ghijkl", versions: [:"tlsv1.3"]) assert opts[:reuse_sessions] == nil assert opts[:secure_renegotiate] == nil assert opts[:honor_cipher_order] == nil assert opts[:client_renegotiation] == nil assert opts[:cipher_suite] == nil - opts = [ - certfile: context.cert_path, - keyfile: context.key_path, - reuse_sessions: false - ] - - assert {:ok, opts} = configure(opts) + assert {:ok, opts} = configure(key: "abcdef", cert: "ghijkl", reuse_sessions: false) assert opts[:reuse_sessions] == false end - test "sets cipher suite to strong", context do - opts = [ - certfile: context.cert_path, - keyfile: context.key_path, - cipher_suite: :strong - ] - - assert {:ok, opts} = configure(opts) + test "sets cipher suite to strong" do + assert {:ok, opts} = configure(key: "abcdef", cert: "ghijkl", cipher_suite: :strong) assert opts[:cipher_suite] == nil assert opts[:honor_cipher_order] == true assert opts[:eccs] == [:x25519, :secp256r1, :secp384r1, :secp521r1] @@ -79,14 +50,8 @@ defmodule Plug.SSLTest do ] end - test "sets cipher suite to compatible", context do - opts = [ - certfile: context.cert_path, - keyfile: context.key_path, - cipher_suite: :compatible - ] - - assert {:ok, opts} = configure(opts) + test "sets cipher suite to compatible" do + assert {:ok, opts} = configure(key: "abcdef", cert: "ghijkl", cipher_suite: :compatible) assert opts[:cipher_suite] == nil assert opts[:honor_cipher_order] == true assert opts[:eccs] == [:x25519, :secp256r1, :secp384r1, :secp521r1] @@ -107,11 +72,11 @@ defmodule Plug.SSLTest do ] end - test "sets cipher suite with overrides compatible", context do + test "sets cipher suite with overrides compatible" do assert {:ok, opts} = configure( - keyfile: context.key_path, - certfile: context.cert_path, + key: "abcdef", + cert: "ghijkl", cipher_suite: :compatible, ciphers: [], client_renegotiation: true, @@ -120,87 +85,100 @@ defmodule Plug.SSLTest do honor_cipher_order: false ) - assert opts[:cipher_suite] == nil - assert opts[:honor_cipher_order] == false - assert opts[:client_renegotiation] == true - assert opts[:eccs] == [] - assert opts[:versions] == [] - assert opts[:ciphers] == [] + assert opts[:cipher_suite] == nil + assert opts[:honor_cipher_order] == false + assert opts[:client_renegotiation] == true + assert opts[:eccs] == [] + assert opts[:versions] == [] + assert opts[:ciphers] == [] end - test "allows bare atom configuration through unchanged", context do - assert {:ok, opts} = - configure([ - :inet6, - {:keyfile, context.key_path}, - {:certfile, context.cert_path} - ]) - + test "allows bare atom configuration through unchanged" do + assert {:ok, opts} = configure([:inet6, {:key, "abcdef"}, {:cert, "ghijkl"}]) assert :inet6 in opts - assert {:keyfile, to_charlist(context.key_path)} in opts - assert {:certfile, to_charlist(context.cert_path)} in opts + assert {:key, "abcdef"} in opts + assert {:cert, "ghijkl"} in opts end test "fails to configure if keyfile and certfile aren't absolute paths and otp_app is missing" do - assert {:error, message} = configure([ - :inet6, - keyfile: "abcdef", - certfile: "ghijkl" - ]) + assert {:error, message} = configure([:inet6, keyfile: "abcdef", certfile: "ghijkl"]) assert message == "the :otp_app option is required when setting relative SSL certfiles" end test "fails to configure if the keyfile doesn't exist" do assert {:error, message} = - configure([:inet6, keyfile: "nonexistent", certfile: "nonexistent", otp_app: :plug]) + configure([:inet6, keyfile: "abcdef", certfile: "ghijkl", otp_app: :plug]) assert message =~ ":keyfile either does not exist, or the application does not have permission to access it" end + test "fails to configure if the certfile doesn't exist" do + File.touch("_build/test/lib/plug/abcdef") + + assert {:error, message} = + configure([:inet6, keyfile: "abcdef", certfile: "ghijkl", otp_app: :plug]) + + assert message =~ + ":certfile either does not exist, or the application does not have permission to access it" + end + test "expands the paths to the keyfile and certfile using the otp_app" do - app_dir = Application.app_dir(:plug) - File.mkdir_p!(app_dir) - File.touch!(Path.join(app_dir, "abcdef")) - File.touch!(Path.join(app_dir, "ghijkl")) + File.touch("_build/test/lib/plug/abcdef") + File.touch("_build/test/lib/plug/ghijkl") + + assert {:ok, opts} = + configure([:inet6, keyfile: "abcdef", certfile: "ghijkl", otp_app: :plug]) + + assert to_string(opts[:keyfile]) =~ "_build/test/lib/plug/abcdef" + assert to_string(opts[:certfile]) =~ "_build/test/lib/plug/ghijkl" + end + + test "supports the certs_keys ssl config option" do + assert {:ok, opts} = + configure([:inet6, certs_keys: [%{key: "abcdef", cert: "ghijkl"}]]) + + assert :inet6 in opts + assert opts[:certs_keys] == [%{key: "abcdef", cert: "ghijkl"}] + end + + test "expands the paths for keyfile and certfile in the certs_keys ssl config option" do + File.touch("_build/test/lib/plug/abcdef") + File.touch("_build/test/lib/plug/ghijkl") assert {:ok, opts} = configure([ :inet6, - keyfile: "abcdef", - certfile: "ghijkl", + certs_keys: [%{keyfile: "abcdef", certfile: "ghijkl"}], otp_app: :plug ]) - assert to_string(opts[:keyfile]) =~ "abcdef" - assert to_string(opts[:certfile]) =~ "ghijkl" + assert :inet6 in opts + + [%{keyfile: keyfile, certfile: certfile}] = opts[:certs_keys] + + assert to_string(keyfile) =~ "_build/test/lib/plug/abcdef" + assert to_string(certfile) =~ "_build/test/lib/plug/ghijkl" end - test "errors when an invalid cipher is given", context do - assert configure( - keyfile: context.key_path, - certfile: context.cert_path, - cipher_suite: :unknown - ) == + test "errors when an invalid cipher is given" do + assert configure(key: "abcdef", cert: "ghijkl", cipher_suite: :unknown) == {:error, "unknown :cipher_suite named :unknown"} end - test "errors when a cipher is provided as a binary string", context do + test "errors when a cipher is provided as a binary string" do assert {:error, message} = configure( - keyfile: context.key_path, - certfile: context.cert_path, - ciphers: [ - ~c"ECDHE-ECDSA-AES256-GCM-SHA384", - "ECDHE-RSA-AES256-GCM-SHA384" - ] + key: "abcdef", + cert: "ghijkl", + ciphers: [~c"ECDHE-ECDSA-AES256-GCM-SHA384", "ECDHE-RSA-AES256-GCM-SHA384"] ) assert message == "invalid cipher \"ECDHE-RSA-AES256-GCM-SHA384\" in cipher list. " <> - "Strings (double-quoted) are not allowed in ciphers. " <> - "Ciphers must be either charlists (single-quoted) or tuples. " <> - "See the ssl application docs for reference" + "Strings (double-quoted) are not allowed in ciphers. " <> + "Ciphers must be either charlists (single-quoted) or tuples. " <> + "See the ssl application docs for reference" end end From 226d1800bd930fbca9d0f68bf8720a5a8b23846b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 29 Aug 2025 20:15:46 +0200 Subject: [PATCH 5/5] Update lib/plug/ssl.ex --- lib/plug/ssl.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/plug/ssl.ex b/lib/plug/ssl.ex index 1d9a49ac..b7689f23 100644 --- a/lib/plug/ssl.ex +++ b/lib/plug/ssl.ex @@ -78,7 +78,7 @@ defmodule Plug.SSL do # TLS 1.3 Ciphersuites ~c"TLS_AES_256_GCM_SHA384", ~c"TLS_CHACHA20_POLY1305_SHA256", - ~c"TLS_AES_128_GCM_SHA256", + ~c"TLS_AES_128_GCM_SHA256" ] @compatible_tls_ciphers [