Files
parrhesia/lib/parrhesia/sync/tls.ex

113 lines
3.3 KiB
Elixir

defmodule Parrhesia.Sync.TLS do
@moduledoc false
@type tls_config :: %{
mode: :required | :disabled,
hostname: String.t(),
pins: [%{type: :spki_sha256, value: String.t()}]
}
@spec websocket_options(tls_config()) :: keyword()
def websocket_options(%{mode: :disabled}), do: [insecure: true]
def websocket_options(%{mode: :required} = tls) do
[
ssl_options: transport_opts(tls)
]
end
@spec req_connect_options(tls_config()) :: keyword()
def req_connect_options(%{mode: :disabled}), do: []
def req_connect_options(%{mode: :required} = tls) do
[
transport_opts: transport_opts(tls)
]
end
def transport_opts(%{hostname: hostname, pins: pins}) do
[
verify: :verify_peer,
cacerts: system_cacerts(),
server_name_indication: String.to_charlist(hostname),
customize_hostname_check: [
match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
]
]
|> maybe_put_verify_fun(pins)
end
defp maybe_put_verify_fun(options, []), do: options
defp maybe_put_verify_fun(options, pins) do
Keyword.put(
options,
:verify_fun,
{&verify_certificate/3, %{pins: MapSet.new(Enum.map(pins, & &1.value)), matched?: false}}
)
end
defp verify_certificate(_cert, :valid_peer, %{matched?: true} = state), do: {:valid, state}
defp verify_certificate(_cert, :valid_peer, _state), do: {:fail, :pin_mismatch}
defp verify_certificate(_cert, {:bad_cert, reason}, _state), do: {:fail, reason}
defp verify_certificate(cert, _event, state) when is_binary(cert) do
matched? = MapSet.member?(state.pins, spki_pin_from_verify(cert))
{:valid, %{state | matched?: state.matched? or matched?}}
rescue
_error -> {:fail, :invalid_certificate}
end
defp verify_certificate({:OTPCertificate, _tbs, _sig_alg, _sig} = cert, _event, state) do
matched? = MapSet.member?(state.pins, spki_pin_from_verify(cert))
{:valid, %{state | matched?: state.matched? or matched?}}
rescue
_error -> {:fail, :invalid_certificate}
end
defp verify_certificate({:Certificate, _tbs, _sig_alg, _sig} = cert, _event, state) do
matched? = MapSet.member?(state.pins, spki_pin_from_verify(cert))
{:valid, %{state | matched?: state.matched? or matched?}}
rescue
_error -> {:fail, :invalid_certificate}
end
defp verify_certificate(_cert, _event, state), do: {:valid, state}
defp spki_pin(cert_der) do
cert = :public_key.pkix_decode_cert(cert_der, :plain)
spki = cert |> elem(1) |> elem(7)
:public_key.der_encode(:SubjectPublicKeyInfo, spki)
|> then(&:crypto.hash(:sha256, &1))
|> Base.encode64()
end
defp spki_pin_from_verify(cert) when is_binary(cert), do: spki_pin(cert)
defp spki_pin_from_verify({:OTPCertificate, _tbs, _sig_alg, _sig} = cert) do
cert
|> then(&:public_key.pkix_encode(:OTPCertificate, &1, :otp))
|> spki_pin()
end
defp spki_pin_from_verify({:Certificate, _tbs, _sig_alg, _sig} = cert) do
cert
|> then(&:public_key.der_encode(:Certificate, &1))
|> spki_pin()
end
defp spki_pin_from_verify(_cert) do
raise(ArgumentError, "invalid certificate")
end
defp system_cacerts do
if function_exported?(:public_key, :cacerts_get, 0) do
:public_key.cacerts_get()
else
[]
end
end
end