1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
|
defmodule Hex.HTTP.SSL do
@moduledoc false
require Record
alias Hex.HTTP.Certs
alias Hex.HTTP.VerifyHostname
# From https://www.ssllabs.com/ssltest/clients.html Android 7
@default_ciphers [
~c"AES128-GCM-SHA256",
~c"AES128-SHA",
~c"AES256-GCM-SHA384",
~c"AES256-SHA",
~c"DES-CBC3-SHA",
~c"ECDHE-ECDSA-AES128-GCM-SHA256",
~c"ECDHE-ECDSA-AES128-SHA",
~c"ECDHE-ECDSA-AES256-GCM-SHA384",
~c"ECDHE-ECDSA-AES256-SHA",
~c"ECDHE-ECDSA-CHACHA20-POLY1305-SHA256",
~c"ECDHE-RSA-AES128-GCM-SHA256",
~c"ECDHE-RSA-AES128-SHA",
~c"ECDHE-RSA-AES256-GCM-SHA384",
~c"ECDHE-RSA-AES256-SHA",
~c"ECDHE-RSA-CHACHA20-POLY1305-SHA256"
]
@default_versions [:"tlsv1.2", :"tlsv1.1", :tlsv1]
@secure_ssl_version {5, 3, 7}
Record.defrecordp(
:certificate,
:OTPCertificate,
Record.extract(:OTPCertificate, from_lib: "public_key/include/OTP-PUB-KEY.hrl")
)
Record.defrecordp(
:tbs_certificate,
:OTPTBSCertificate,
Record.extract(:OTPTBSCertificate, from_lib: "public_key/include/OTP-PUB-KEY.hrl")
)
def secure_ssl? do
check? = not Hex.State.fetch!(:unsafe_https)
if check? and Hex.State.fetch!(:ssl_version) <= @secure_ssl_version do
Mix.raise(
"Insecure HTTPS request (peer verification disabled), " <>
"please update to OTP 17.4 or later, or disable by setting " <>
"the environment variable HEX_UNSAFE_HTTPS=1"
)
end
check?
end
def get_ca_certs do
case Hex.State.fetch!(:cacerts_path) do
nil -> Certs.cacerts()
path -> Certs.decode_runtime(path)
end
end
def ssl_opts(url) do
hostname = String.to_charlist(URI.parse(url).host)
ciphers = filter_ciphers(@default_ciphers)
if secure_ssl?() do
partial_chain = &partial_chain(Certs.cacerts(), &1)
[
verify: :verify_peer,
depth: 4,
partial_chain: partial_chain,
cacerts: get_ca_certs(),
server_name_indication: hostname,
secure_renegotiate: true,
reuse_sessions: true,
versions: @default_versions,
ciphers: ciphers
]
|> customize_hostname_check(hostname)
else
[
verify: :verify_none,
server_name_indication: hostname,
secure_renegotiate: true,
reuse_sessions: true,
versions: @default_versions,
ciphers: ciphers
]
end
end
def partial_chain(cacerts, certs) do
certs = Enum.map(certs, &{&1, :public_key.pkix_decode_cert(&1, :otp)})
cacerts = Enum.map(cacerts, &:public_key.pkix_decode_cert(&1, :otp))
trusted =
Enum.find_value(certs, fn {der, cert} ->
trusted? =
Enum.find(cacerts, fn cacert ->
extract_public_key_info(cacert) == extract_public_key_info(cert)
end)
if trusted?, do: der
end)
if trusted do
{:trusted_ca, trusted}
else
:unknown_ca
end
end
defp extract_public_key_info(cert) do
cert
|> certificate(:tbsCertificate)
|> tbs_certificate(:subjectPublicKeyInfo)
end
defp filter_ciphers(allowed) do
available = MapSet.new(Hex.Stdlib.ssl_cipher_suites(:openssl))
Enum.filter(allowed, &(&1 in available))
end
defp customize_hostname_check(opts, hostname) do
if ssl_major_version() >= 9 do
# From OTP 20.0 use built-in support for custom hostname checks
Keyword.put(opts, :customize_hostname_check, match_fun: &VerifyHostname.match_fun/2)
else
# Before OTP 20.0 use mint_shims for hostname check, from a custom verify_fun
Keyword.put(opts, :verify_fun, {&VerifyHostname.verify_fun/3, check_hostname: hostname})
end
end
defp ssl_major_version do
# Elixir 1.0.5 - 1.1.1 have no Application.spec/2
case :application.get_key(:ssl, :vsn) do
{:ok, value} -> value
:undefined -> nil
end
|> :string.to_integer()
|> elem(0)
end
end
|