ssl.ex (13212B)
1 defmodule Plug.SSL do 2 @moduledoc """ 3 A plug to force SSL connections and enable HSTS. 4 5 If the scheme of a request is `https`, it'll add a `strict-transport-security` 6 header to enable HTTP Strict Transport Security by default. 7 8 Otherwise, the request will be redirected to a corresponding location 9 with the `https` scheme by setting the `location` header of the response. 10 The status code will be 301 if the method of `conn` is `GET` or `HEAD`, 11 or 307 in other situations. 12 13 Besides being a Plug, this module also provides conveniences for configuring 14 SSL. See `configure/1`. 15 16 ## x-forwarded-* 17 18 If your Plug application is behind a proxy that handles HTTPS, you may 19 need to tell Plug to parse the proper protocol from the `x-forwarded-*` 20 header. This can be done using the `:rewrite_on` option: 21 22 plug Plug.SSL, rewrite_on: [:x_forwarded_host, :x_forwarded_port, :x_forwarded_proto] 23 24 For further details refer to `Plug.RewriteOn`. 25 26 ## Plug Options 27 28 * `:rewrite_on` - rewrites the given connection information based on the given headers 29 * `:hsts` - a boolean on enabling HSTS or not, defaults to `true` 30 * `:expires` - seconds to expires for HSTS, defaults to `31_536_000` (1 year) 31 * `:preload` - a boolean to request inclusion on the HSTS preload list 32 (for full set of required flags, see: [Chromium HSTS submission site](https://hstspreload.org)), 33 defaults to `false` 34 * `:subdomains` - a boolean on including subdomains or not in HSTS, 35 defaults to `false` 36 * `:exclude` - exclude the given hosts from redirecting to the `https` 37 scheme. Defaults to `["localhost"]`. It may be set to a list of binaries 38 or a tuple [`{module, function, args}`](#module-excluded-hosts-tuple). 39 * `:host` - a new host to redirect to if the request's scheme is `http`, 40 defaults to `conn.host`. It may be set to a binary or a tuple 41 `{module, function, args}` that will be invoked on demand 42 * `:log` - The log level at which this plug should log its request info. 43 Default is `:info`. Can be `false` to disable logging. 44 45 ## Port 46 47 It is not possible to directly configure the port in `Plug.SSL` because 48 HSTS expects the port to be 443 for SSL. If you are not using HSTS and 49 want to redirect to HTTPS on another port, you can sneak it alongside 50 the host, for example: `host: "example.com:443"`. 51 52 ## Excluded hosts tuple 53 54 Tuple `{module, function, args}` can be passed to be invoked each time 55 the plug is checking whether to redirect host. Provided function needs 56 to receive at least one argument (`host`). 57 58 For example, you may define it as: 59 60 plug Plug.SSL, 61 rewrite_on: [:x_forwarded_proto], 62 exclude: {__MODULE__, :excluded_host?, []} 63 64 where: 65 66 def excluded_host?(host) do 67 # Custom logic 68 end 69 70 """ 71 @behaviour Plug 72 73 require Logger 74 import Plug.Conn 75 76 @strong_tls_ciphers [ 77 'ECDHE-RSA-AES256-GCM-SHA384', 78 'ECDHE-ECDSA-AES256-GCM-SHA384', 79 'ECDHE-RSA-AES128-GCM-SHA256', 80 'ECDHE-ECDSA-AES128-GCM-SHA256', 81 'DHE-RSA-AES256-GCM-SHA384', 82 'DHE-RSA-AES128-GCM-SHA256' 83 ] 84 85 @compatible_tls_ciphers [ 86 'ECDHE-RSA-AES256-GCM-SHA384', 87 'ECDHE-ECDSA-AES256-GCM-SHA384', 88 'ECDHE-RSA-AES128-GCM-SHA256', 89 'ECDHE-ECDSA-AES128-GCM-SHA256', 90 'DHE-RSA-AES256-GCM-SHA384', 91 'DHE-RSA-AES128-GCM-SHA256', 92 'ECDHE-RSA-AES256-SHA384', 93 'ECDHE-ECDSA-AES256-SHA384', 94 'ECDHE-RSA-AES128-SHA256', 95 'ECDHE-ECDSA-AES128-SHA256', 96 'DHE-RSA-AES256-SHA256', 97 'DHE-RSA-AES128-SHA256', 98 'ECDHE-RSA-AES256-SHA', 99 'ECDHE-ECDSA-AES256-SHA', 100 'ECDHE-RSA-AES128-SHA', 101 'ECDHE-ECDSA-AES128-SHA' 102 ] 103 104 @eccs [ 105 :secp256r1, 106 :secp384r1, 107 :secp521r1 108 ] 109 110 @doc """ 111 Configures and validates the options given to the `:ssl` application. 112 113 This function is often called internally by adapters, such as Cowboy, 114 to validate and set reasonable defaults for SSL handling. Therefore 115 Plug users are not expected to invoke it directly, rather you pass 116 the relevant SSL options to your adapter which then invokes this. 117 118 ## Options 119 120 This function accepts all options defined 121 [in Erlang/OTP `:ssl` documentation](http://erlang.org/doc/man/ssl.html). 122 123 Besides the options from `:ssl`, this function adds on extra option: 124 125 * `:cipher_suite` - it may be `:strong` or `:compatible`, 126 as outlined in the following section 127 128 Furthermore, it sets the following defaults: 129 130 * `secure_renegotiate: true` - to avoid certain types of man-in-the-middle attacks 131 * `reuse_sessions: true` - for improved handshake performance of recurring connections 132 133 For a complete guide on HTTPS and best pratices, see [our Plug HTTPS Guide](https.html). 134 135 ## Cipher Suites 136 137 To simplify configuration of TLS defaults, this function provides two preconfigured 138 options: `cipher_suite: :strong` and `cipher_suite: :compatible`. The Ciphers 139 chosen and related configuration come from the [OWASP Cipher String Cheat 140 Sheet](https://www.owasp.org/index.php/TLS_Cipher_String_Cheat_Sheet) 141 142 We've made two modifications to the suggested config from the OWASP recommendations. 143 First we include ECDSA certificates which are excluded from their configuration. 144 Second we have changed the order of the ciphers to deprioritize DHE because of 145 performance implications noted within the OWASP post itself. As the article notes 146 "...the TLS handshake with DHE hinders the CPU about 2.4 times more than ECDHE". 147 148 The **Strong** cipher suite only supports tlsv1.2. Ciphers were based on the OWASP 149 Group A+ and includes support for RSA or ECDSA certificates. The intention of this 150 configuration is to provide as secure as possible defaults knowing that it will not 151 be fully compatible with older browsers and operating systems. 152 153 The **Compatible** cipher suite supports tlsv1, tlsv1.1 and tlsv1.2. Ciphers were 154 based on the OWASP Group B and includes support for RSA or ECDSA certificates. The 155 intention of this configuration is to provide as secure as possible defaults that 156 still maintain support for older browsers and Android versions 4.3 and earlier 157 158 For both suites we've specified certificate curves secp256r1, ecp384r1 and secp521r1. 159 Since OWASP doesn't prescribe curves we've based the selection on [Mozilla's 160 recommendations](https://wiki.mozilla.org/Security/Server_Side_TLS#Cipher_names_correspondence_table) 161 162 **The cipher suites were last updated on 2018-JUN-14.** 163 """ 164 @spec configure(Keyword.t()) :: {:ok, Keyword.t()} | {:error, String.t()} 165 def configure(options) do 166 options 167 |> check_for_missing_keys() 168 |> validate_ciphers() 169 |> normalize_ssl_files() 170 |> convert_to_charlist() 171 |> set_secure_defaults() 172 |> configure_managed_tls() 173 catch 174 {:configure, message} -> {:error, message} 175 else 176 options -> {:ok, options} 177 end 178 179 defp check_for_missing_keys(options) do 180 has_sni? = Keyword.has_key?(options, :sni_hosts) or Keyword.has_key?(options, :sni_fun) 181 has_key? = Keyword.has_key?(options, :key) or Keyword.has_key?(options, :keyfile) 182 has_cert? = Keyword.has_key?(options, :cert) or Keyword.has_key?(options, :certfile) 183 184 cond do 185 has_sni? -> options 186 not has_key? -> fail("missing option :key/:keyfile") 187 not has_cert? -> fail("missing option :cert/:certfile") 188 true -> options 189 end 190 end 191 192 defp normalize_ssl_files(options) do 193 ssl_files = [:keyfile, :certfile, :cacertfile, :dhfile] 194 Enum.reduce(ssl_files, options, &normalize_ssl_file(&1, &2)) 195 end 196 197 defp normalize_ssl_file(key, options) do 198 value = options[key] 199 200 cond do 201 is_nil(value) -> 202 options 203 204 Path.type(value) == :absolute -> 205 put_ssl_file(options, key, value) 206 207 true -> 208 put_ssl_file(options, key, Path.expand(value, otp_app(options))) 209 end 210 end 211 212 defp put_ssl_file(options, key, value) do 213 value = to_charlist(value) 214 215 unless File.exists?(value) do 216 message = 217 "the file #{value} required by SSL's #{inspect(key)} either does not exist, " <> 218 "or the application does not have permission to access it" 219 220 fail(message) 221 end 222 223 Keyword.put(options, key, value) 224 end 225 226 defp otp_app(options) do 227 if app = options[:otp_app] do 228 Application.app_dir(app) 229 else 230 fail("the :otp_app option is required when setting relative SSL certfiles") 231 end 232 end 233 234 defp convert_to_charlist(options) do 235 Enum.reduce([:password], options, fn key, acc -> 236 if value = acc[key] do 237 Keyword.put(acc, key, to_charlist(value)) 238 else 239 acc 240 end 241 end) 242 end 243 244 defp set_secure_defaults(options) do 245 versions = options[:versions] || :ssl.versions()[:supported] 246 247 if Enum.any?([:tlsv1, :"tlsv1.1", :"tlsv1.2"], &(&1 in versions)) do 248 options 249 |> Keyword.put_new(:secure_renegotiate, true) 250 |> Keyword.put_new(:reuse_sessions, true) 251 else 252 options 253 |> Keyword.delete(:secure_renegotiate) 254 |> Keyword.delete(:reuse_sessions) 255 end 256 end 257 258 defp configure_managed_tls(options) do 259 {cipher_suite, options} = Keyword.pop(options, :cipher_suite) 260 261 case cipher_suite do 262 :strong -> set_strong_tls_defaults(options) 263 :compatible -> set_compatible_tls_defaults(options) 264 nil -> options 265 _ -> fail("unknown :cipher_suite named #{inspect(cipher_suite)}") 266 end 267 end 268 269 defp set_managed_tls_defaults(options) do 270 options 271 |> Keyword.put_new(:honor_cipher_order, true) 272 |> Keyword.put_new(:eccs, @eccs) 273 end 274 275 defp set_strong_tls_defaults(options) do 276 options 277 |> set_managed_tls_defaults 278 |> Keyword.put_new(:ciphers, @strong_tls_ciphers) 279 |> Keyword.put_new(:versions, [:"tlsv1.2"]) 280 end 281 282 defp set_compatible_tls_defaults(options) do 283 options 284 |> set_managed_tls_defaults 285 |> Keyword.put_new(:ciphers, @compatible_tls_ciphers) 286 |> Keyword.put_new(:versions, [:"tlsv1.2", :"tlsv1.1", :tlsv1]) 287 end 288 289 defp validate_ciphers(options) do 290 options 291 |> Keyword.get(:ciphers, []) 292 |> Enum.each(&validate_cipher/1) 293 294 options 295 end 296 297 defp validate_cipher(cipher) do 298 if is_binary(cipher) do 299 message = 300 "invalid cipher #{inspect(cipher)} in cipher list. " <> 301 "Strings (double-quoted) are not allowed in ciphers. " <> 302 "Ciphers must be either charlists (single-quoted) or tuples. " <> 303 "See the ssl application docs for reference" 304 305 fail(message) 306 end 307 end 308 309 defp fail(message) when is_binary(message) do 310 throw({:configure, message}) 311 end 312 313 @impl true 314 def init(opts) do 315 host = Keyword.get(opts, :host) 316 317 case host do 318 {:system, _} -> 319 IO.warn( 320 "Using {:system, host} as your Plug.SSL host is deprecated. Pass nil or a string instead." 321 ) 322 323 _ -> 324 :ok 325 end 326 327 rewrite_on = Plug.RewriteOn.init(Keyword.get(opts, :rewrite_on)) 328 log = Keyword.get(opts, :log, :info) 329 exclude = Keyword.get(opts, :exclude, ["localhost"]) 330 {hsts_header(opts), exclude, host, rewrite_on, log} 331 end 332 333 @impl true 334 def call(conn, {hsts, exclude, host, rewrite_on, log_level}) do 335 conn = Plug.RewriteOn.call(conn, rewrite_on) 336 337 cond do 338 excluded?(conn.host, exclude) -> conn 339 conn.scheme == :https -> put_hsts_header(conn, hsts) 340 true -> redirect_to_https(conn, host, log_level) 341 end 342 end 343 344 defp excluded?(host, list) when is_list(list), do: :lists.member(host, list) 345 defp excluded?(host, {mod, fun, args}), do: apply(mod, fun, [host | args]) 346 347 # http://tools.ietf.org/html/draft-hodges-strict-transport-sec-02 348 defp hsts_header(opts) do 349 if Keyword.get(opts, :hsts, true) do 350 expires = Keyword.get(opts, :expires, 31_536_000) 351 preload = Keyword.get(opts, :preload, false) 352 subdomains = Keyword.get(opts, :subdomains, false) 353 354 "max-age=#{expires}" <> 355 if(preload, do: "; preload", else: "") <> 356 if(subdomains, do: "; includeSubDomains", else: "") 357 end 358 end 359 360 defp put_hsts_header(conn, hsts_header) when is_binary(hsts_header) do 361 put_resp_header(conn, "strict-transport-security", hsts_header) 362 end 363 364 defp put_hsts_header(conn, nil), do: conn 365 366 defp redirect_to_https(%{host: host} = conn, custom_host, log_level) do 367 status = if conn.method in ~w(HEAD GET), do: 301, else: 307 368 369 scheme_and_host = "https://" <> host(custom_host, host) 370 location = scheme_and_host <> conn.request_path <> qs(conn.query_string) 371 372 log_level && 373 Logger.log(log_level, fn -> 374 [ 375 "Plug.SSL is redirecting ", 376 conn.method, 377 ?\s, 378 conn.request_path, 379 " to ", 380 scheme_and_host, 381 " with status ", 382 Integer.to_string(status) 383 ] 384 end) 385 386 conn 387 |> put_resp_header("location", location) 388 |> send_resp(status, "") 389 |> halt 390 end 391 392 defp host(nil, host), do: host 393 defp host(host, _) when is_binary(host), do: host 394 defp host({mod, fun, args}, host), do: host(apply(mod, fun, args), host) 395 # TODO: Remove me once the deprecation is removed. 396 defp host({:system, env}, host), do: host(System.get_env(env), host) 397 398 defp qs(""), do: "" 399 defp qs(qs), do: "?" <> qs 400 end