zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

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