diff --git a/lib/chronoscope/gemini.ex b/lib/chronoscope/gemini.ex index 4ace4db..b7ca140 100644 --- a/lib/chronoscope/gemini.ex +++ b/lib/chronoscope/gemini.ex @@ -1,3 +1,2 @@ defmodule Chronoscope.Gemini do - end diff --git a/lib/chronoscope/nts.ex b/lib/chronoscope/nts.ex index ef72037..a9df84a 100644 --- a/lib/chronoscope/nts.ex +++ b/lib/chronoscope/nts.ex @@ -1,3 +1,38 @@ defmodule Chronoscope.NTS do - + require Logger + + alias Chronoscope.NTS.KeyEstablishment + + @timeout_in_milliseconds 3000 + + def key_establishment(host, port) do + tls_options = :tls_certificate_check.options(host) ++ [alpn_advertised_protocols: ["ntske/1"]] + + case :ssl.connect(host, port, tls_options, @timeout_in_milliseconds) do + {:ok, socket} -> perform_key_establishment(socket) + {:error, {:tls_alert, {:handshake_failure, error}}} -> {:error, to_string(error)} + {:error, error} -> {:error, inspect(error)} + error -> {:error, inspect(error)} + end + end + + defp perform_key_establishment(socket) do + :ok = :ssl.send(socket, KeyEstablishment.request()) + + receive do + {:ssl, _socket, response} -> + :ssl.close(socket) + KeyEstablishment.parse_response(response) + + msg -> + :ssl.close(socket) + Logger.error("received unexpected message: #{inspect(msg)}") + {:error, :no_response} + after + @timeout_in_milliseconds -> + :ssl.close(socket) + Logger.error("timed out waiting for response") + {:error, :timeout} + end + end end diff --git a/lib/chronoscope/nts/key_establishment.ex b/lib/chronoscope/nts/key_establishment.ex new file mode 100644 index 0000000..0679aaa --- /dev/null +++ b/lib/chronoscope/nts/key_establishment.ex @@ -0,0 +1,90 @@ +defmodule Chronoscope.NTS.KeyEstablishment do + import Bitwise + + @next_protocol_negotiation <<0x80, 0x01, 0x00, 0x02, 0x00, 0x00>> + @aead_algorithm_negotiation <<0x80, 0x04, 0x00, 0x04, 0x00, 0x1E, 0x00, 0x0F>> + @end_of_message <<0x80, 0x00, 0x00, 0x00>> + + def request() do + @next_protocol_negotiation <> @aead_algorithm_negotiation <> @end_of_message + end + + def parse_response(response) do + {:ok, Map.update(do_parse_response(response, %{}), :cookies, 0, &length/1)} + end + + defp do_parse_response([], acc) do + acc + end + + defp do_parse_response([0x80, 0x00, 0x00, 0x00], acc) do + acc + end + + defp do_parse_response([0x80, 0x01, length_high, length_low | rest], acc) do + length = combine_octets(length_high, length_low) + {next_protocols, remaining} = Enum.split(rest, length) + + do_parse_response( + remaining, + Map.put(acc, :next_protocols, parse_next_protocol(next_protocols)) + ) + end + + defp do_parse_response([first, 0x04, length_high, length_low | rest], acc) + when first == 0x00 or first == 0x80 do + length = combine_octets(length_high, length_low) + {aead_algorithms, remaining} = Enum.split(rest, length) + + do_parse_response( + remaining, + Map.put(acc, :aead_algorithms, parse_aead_algorithm(aead_algorithms)) + ) + end + + defp do_parse_response([first, 0x05, length_high, length_low | rest], acc) + when first == 0x00 or first == 0x80 do + length = combine_octets(length_high, length_low) + {cookie, remaining} = Enum.split(rest, length) + + do_parse_response( + remaining, + acc + |> Map.update(:cookies, [cookie], &[cookie | &1]) + |> Map.put(:cookie_length, length) + ) + end + + defp do_parse_response([_, _, length_high, length_low | rest], acc) do + length = combine_octets(length_high, length_low) + {_, remaining} = Enum.split(rest, length) + + do_parse_response(remaining, acc) + end + + defp parse_aead_algorithm([0x00, 0x0F]) do + "AEAD_AES_SIV_CMAC_256" + end + + defp parse_aead_algorithm([0x00, 0x1E]) do + "AEAD_AES_128_GCM_SIV" + end + + defp parse_aead_algorithm(_aead_algorithm) do + "UNKNOWN" + end + + defp parse_next_protocol([0x00, 0x00]) do + "NTPv4" + end + + defp parse_next_protocol(_next_protocol) do + "UNASSIGNED" + end + + # todo parse server/port information + + defp combine_octets(high, low) do + high <<< 8 ||| low + end +end diff --git a/lib/chronoscope/nts/key_exchange.ex b/lib/chronoscope/nts/key_exchange.ex deleted file mode 100644 index c3bf464..0000000 --- a/lib/chronoscope/nts/key_exchange.ex +++ /dev/null @@ -1,2 +0,0 @@ -defmodule Chronoscope.NTS.KeyExchange do -end diff --git a/lib/chronoscope_web/controllers/api/nts/key_exchange_controller.ex b/lib/chronoscope_web/controllers/api/nts/key_exchange_controller.ex index 07735f4..b734a94 100644 --- a/lib/chronoscope_web/controllers/api/nts/key_exchange_controller.ex +++ b/lib/chronoscope_web/controllers/api/nts/key_exchange_controller.ex @@ -3,58 +3,23 @@ defmodule ChronoscopeWeb.API.NTS.KeyExchangeController do require Logger + alias Chronoscope.NTS + @default_port "4460" - @timeout_in_milliseconds 3000 def get(conn, params) do host = to_charlist(params["host"]) port = String.to_integer(params["port"] || @default_port) - tls_options = :tls_certificate_check.options(host) ++ [alpn_advertised_protocols: ["ntse/1"]] - - case :ssl.connect(host, port, tls_options, @timeout_in_milliseconds) do - {:ok, socket} -> - key_exchange_request = - <<0x80, 0x01, 0x00, 0x02, 0x00, 0x00, 0x80, 0x04, 0x00, 0x04, 0x00, 0x1E, 0x00, 0x0F, - 0x80, 0x00, 0x00, 0x00>> - - :ok = :ssl.send(socket, key_exchange_request) - - receive do - {:ssl, _socket, data} -> - :ssl.close(socket) - ok_response(conn, %{status: :ok, response: parse_response(data)}) - - msg -> - :ssl.close(socket) - Logger.error("received unexpected message: #{inspect(msg)}") - ok_response(conn, %{status: :error, reason: :no_response}) - after - @timeout_in_milliseconds -> - :ssl.close(socket) - Logger.error("timed out waiting for response") - ok_response(conn, %{status: :error, reason: :timeout}) - end - - {:error, {:tls_alert, {:handshake_failure, error}}} -> - ok_response(conn, %{status: :error, reason: to_string(error)}) + case NTS.key_establishment(host, port) do + {:ok, configuration} -> + ok_response(conn, %{status: :ok, configuration: configuration}) {:error, error} -> ok_response(conn, %{status: :error, reason: inspect(error)}) - - error -> - ok_response(conn, %{status: :error, reason: inspect(error)}) end end - defp parse_response([0x80 | _] = _data) do - "success" - end - - defp parse_response(_data) do - "failure" - end - defp ok_response(conn, body) do conn |> Plug.Conn.put_resp_content_type("application/json")