Skip to content

Commit 187266d

Browse files
authored
Add SSL validation support for certs_keys (#1260)
1 parent f941c3c commit 187266d

File tree

2 files changed

+102
-8
lines changed

2 files changed

+102
-8
lines changed

lib/plug/ssl.ex

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ defmodule Plug.SSL do
169169
|> check_for_missing_keys()
170170
|> validate_ciphers()
171171
|> normalize_ssl_files()
172+
|> normalize_certs_keys_ssl_files()
172173
|> convert_to_charlist()
173174
|> set_secure_defaults()
174175
|> configure_managed_tls()
@@ -179,24 +180,45 @@ defmodule Plug.SSL do
179180
end
180181

181182
defp check_for_missing_keys(options) do
183+
has_certs_keys? = List.keymember?(options, :certs_keys, 0)
182184
has_sni? = List.keymember?(options, :sni_hosts, 0) or List.keymember?(options, :sni_fun, 0)
183185
has_key? = List.keymember?(options, :key, 0) or List.keymember?(options, :keyfile, 0)
184186
has_cert? = List.keymember?(options, :cert, 0) or List.keymember?(options, :certfile, 0)
185187

186188
cond do
187189
has_sni? -> options
188-
not has_key? -> fail("missing option :key/:keyfile")
189-
not has_cert? -> fail("missing option :cert/:certfile")
190+
not (has_key? or has_certs_keys?) -> fail("missing option :key/:keyfile/:certs_keys")
191+
not (has_cert? or has_certs_keys?) -> fail("missing option :cert/:certfile/:certs_keys")
190192
true -> options
191193
end
192194
end
193195

194196
defp normalize_ssl_files(options) do
195197
ssl_files = [:keyfile, :certfile, :cacertfile, :dhfile]
196-
Enum.reduce(ssl_files, options, &normalize_ssl_file(&1, &2))
198+
Enum.reduce(ssl_files, options, &normalize_ssl_file(&1, &2, options[:otp_app]))
197199
end
198200

199-
defp normalize_ssl_file(key, options) do
201+
defp normalize_certs_keys_ssl_files(options) do
202+
if certs_keys = options[:certs_keys] do
203+
ssl_files = [:keyfile, :certfile]
204+
205+
updated_certs_keys =
206+
Enum.map(certs_keys, fn cert_key ->
207+
Enum.reduce(
208+
ssl_files,
209+
Map.to_list(cert_key),
210+
&normalize_ssl_file(&1, &2, options[:otp_app])
211+
)
212+
|> Map.new()
213+
end)
214+
215+
List.keystore(options, :certs_keys, 0, {:certs_keys, updated_certs_keys})
216+
else
217+
options
218+
end
219+
end
220+
221+
defp normalize_ssl_file(key, options, otp_app) do
200222
value = options[key]
201223

202224
cond do
@@ -207,7 +229,7 @@ defmodule Plug.SSL do
207229
put_ssl_file(options, key, value)
208230

209231
true ->
210-
put_ssl_file(options, key, Path.expand(value, otp_app(options)))
232+
put_ssl_file(options, key, Path.expand(value, resolve_otp_app(otp_app)))
211233
end
212234
end
213235

@@ -225,9 +247,9 @@ defmodule Plug.SSL do
225247
List.keystore(options, key, 0, {key, value})
226248
end
227249

228-
defp otp_app(options) do
229-
if app = options[:otp_app] do
230-
Application.app_dir(app)
250+
defp resolve_otp_app(otp_app) do
251+
if otp_app do
252+
Application.app_dir(otp_app)
231253
else
232254
fail("the :otp_app option is required when setting relative SSL certfiles")
233255
end

test/plug/ssl_test.exs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ defmodule Plug.SSLTest do
44
import Plug.Conn
55

66
describe "configure" do
7+
# make sure some dummy files used for the keyfile and certfile
8+
# tests are removed after each test.
9+
setup do
10+
on_exit(fn ->
11+
File.rm("_build/test/lib/plug/abcdef")
12+
File.rm("_build/test/lib/plug/ghijkl")
13+
end)
14+
15+
[]
16+
end
17+
718
import Plug.SSL, only: [configure: 1]
819

920
test "sets secure_renegotiate and reuse_sessions to true depending on the version" do
@@ -97,6 +108,67 @@ defmodule Plug.SSLTest do
97108
assert {:cert, "ghijkl"} in opts
98109
end
99110

111+
test "fails to configure if keyfile and certfile aren't absolute paths and otp_app is missing" do
112+
assert {:error, message} = configure([:inet6, keyfile: "abcdef", certfile: "ghijkl"])
113+
assert message == "the :otp_app option is required when setting relative SSL certfiles"
114+
end
115+
116+
test "fails to configure if the keyfile doesn't exist" do
117+
assert {:error, message} =
118+
configure([:inet6, keyfile: "abcdef", certfile: "ghijkl", otp_app: :plug])
119+
120+
assert message =~
121+
":keyfile either does not exist, or the application does not have permission to access it"
122+
end
123+
124+
test "fails to configure if the certfile doesn't exist" do
125+
File.touch("_build/test/lib/plug/abcdef")
126+
127+
assert {:error, message} =
128+
configure([:inet6, keyfile: "abcdef", certfile: "ghijkl", otp_app: :plug])
129+
130+
assert message =~
131+
":certfile either does not exist, or the application does not have permission to access it"
132+
end
133+
134+
test "expands the paths to the keyfile and certfile using the otp_app" do
135+
File.touch("_build/test/lib/plug/abcdef")
136+
File.touch("_build/test/lib/plug/ghijkl")
137+
138+
assert {:ok, opts} =
139+
configure([:inet6, keyfile: "abcdef", certfile: "ghijkl", otp_app: :plug])
140+
141+
assert to_string(opts[:keyfile]) =~ "_build/test/lib/plug/abcdef"
142+
assert to_string(opts[:certfile]) =~ "_build/test/lib/plug/ghijkl"
143+
end
144+
145+
test "supports the certs_keys ssl config option" do
146+
assert {:ok, opts} =
147+
configure([:inet6, certs_keys: [%{key: "abcdef", cert: "ghijkl"}]])
148+
149+
assert :inet6 in opts
150+
assert opts[:certs_keys] == [%{key: "abcdef", cert: "ghijkl"}]
151+
end
152+
153+
test "expands the paths for keyfile and certfile in the certs_keys ssl config option" do
154+
File.touch("_build/test/lib/plug/abcdef")
155+
File.touch("_build/test/lib/plug/ghijkl")
156+
157+
assert {:ok, opts} =
158+
configure([
159+
:inet6,
160+
certs_keys: [%{keyfile: "abcdef", certfile: "ghijkl"}],
161+
otp_app: :plug
162+
])
163+
164+
assert :inet6 in opts
165+
166+
[%{keyfile: keyfile, certfile: certfile}] = opts[:certs_keys]
167+
168+
assert to_string(keyfile) =~ "_build/test/lib/plug/abcdef"
169+
assert to_string(certfile) =~ "_build/test/lib/plug/ghijkl"
170+
end
171+
100172
test "errors when an invalid cipher is given" do
101173
assert configure(key: "abcdef", cert: "ghijkl", cipher_suite: :unknown) ==
102174
{:error, "unknown :cipher_suite named :unknown"}

0 commit comments

Comments
 (0)