From ac3b7ebcc7e34bbfeffccf0a94c25d8075c648f5 Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Sat, 23 Mar 2024 12:19:13 -0400 Subject: [PATCH] Cache NTS key exchange responses --- lib/chronoscope/application.ex | 3 +- lib/chronoscope/nts.ex | 60 ++----- lib/chronoscope/nts/client.ex | 80 +++++++++ .../nts/key_establishment_client.ex | 64 ++++++++ .../nts/key_establishment_request.ex | 9 ++ ...hment.ex => key_establishment_response.ex} | 50 +++--- .../controllers/api/v1/health_controller.ex | 4 +- .../controllers/api/v1/nts/nts_client.ex | 0 .../nts/key_establishment_request_test.exs | 10 ++ .../nts/key_establishment_response_test.exs | 144 +++++++++++++++++ .../nts/key_establishment_test.exs | 153 ------------------ 11 files changed, 342 insertions(+), 235 deletions(-) create mode 100644 lib/chronoscope/nts/client.ex create mode 100644 lib/chronoscope/nts/key_establishment_client.ex create mode 100644 lib/chronoscope/nts/key_establishment_request.ex rename lib/chronoscope/nts/{key_establishment.ex => key_establishment_response.ex} (61%) delete mode 100644 lib/chronoscope_web/controllers/api/v1/nts/nts_client.ex create mode 100644 test/chronoscope/nts/key_establishment_request_test.exs create mode 100644 test/chronoscope/nts/key_establishment_response_test.exs delete mode 100644 test/chronoscope/nts/key_establishment_test.exs diff --git a/lib/chronoscope/application.ex b/lib/chronoscope/application.ex index 47745dc..dbf3c4e 100644 --- a/lib/chronoscope/application.ex +++ b/lib/chronoscope/application.ex @@ -16,7 +16,8 @@ defmodule Chronoscope.Application do # Start a worker by calling: Chronoscope.Worker.start_link(arg) # {Chronoscope.Worker, arg}, # Start to serve requests, typically the last entry - ChronoscopeWeb.Endpoint + ChronoscopeWeb.Endpoint, + Chronoscope.NTS.Client ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/lib/chronoscope/nts.ex b/lib/chronoscope/nts.ex index 6f16176..8805987 100644 --- a/lib/chronoscope/nts.ex +++ b/lib/chronoscope/nts.ex @@ -7,59 +7,17 @@ defmodule Chronoscope.NTS do require Logger - alias Chronoscope.NTS.KeyEstablishment + alias Chronoscope.NTS.Client - @timeout_in_milliseconds 3000 + def healthy?() do + GenServer.call(Client, :healthy?) + end + + def list() do + GenServer.call(Client, :list) + end def key_establishment(host, port) do - case ssl_connect(host, port) do - {:ok, socket} -> perform_key_establishment(socket) - {:error, {:tls_alert, {:handshake_failure, error}}} -> {:error, to_string(error)} - {:error, :timeout} -> {:error, :timeout} - {:error, error} -> {:error, inspect(error)} - error -> {:error, inspect(error)} - end - end - - defp ssl_connect(host, port) do - :ssl.connect(host, port, tls_options(host), @timeout_in_milliseconds) - end - - defp tls_options(host) do - host - |> :tls_certificate_check.options() - |> Keyword.put(:alpn_advertised_protocols, ["ntske/1"]) - |> Keyword.put(:verify_fun, {&verify_fun/3, [check_hostname: host]}) - end - - defp verify_fun(cert, :valid_peer = event, intial_user_state) do - {:Validity, {:utcTime, _from}, {:utcTime, _to}} = X509.Certificate.validity(cert) - # parse datetime from messy erlang version and store in nts client genserver - - :ssl_verify_hostname.verify_fun(cert, event, intial_user_state) - end - - defp verify_fun(cert, event, initial_user_state) do - :ssl_verify_hostname.verify_fun(cert, event, initial_user_state) - 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 + GenServer.call(Client, {:key_establishment, %{host: host, port: port}}) end end diff --git a/lib/chronoscope/nts/client.ex b/lib/chronoscope/nts/client.ex new file mode 100644 index 0000000..db3eda4 --- /dev/null +++ b/lib/chronoscope/nts/client.ex @@ -0,0 +1,80 @@ +defmodule Chronoscope.NTS.Client do + use GenServer + + alias Chronoscope.NTS.KeyEstablishmentClient + + @interval_in_seconds 30 + + def start_link(_) do + GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + end + + @impl true + def init(_) do + {:ok, %{}} + end + + @impl true + def handle_call(:healthy?, _from, state) do + {:reply, true, state} + end + + @impl true + def handle_call(:list, _from, state) do + {:reply, state, state} + end + + @impl true + def handle_call({:key_establishment, server}, _from, state) do + new_state = update_state(server, state) + + {:reply, get_response(server, new_state), new_state} + end + + # fixme - remove + @impl true + def handle_info(_, state) do + {:noreply, state} + end + + defp update_state(server, state) do + now = DateTime.utc_now() + last_key_establishment = get_last_key_establishment(server, state, now) + + if interval_surpassed?(now, last_key_establishment) do + Map.put(state, server, current_data(server, now)) + else + state + end + end + + defp get_response(server, state) do + state + |> Map.get(server) + |> Map.get(:key_establishment_response) + end + + defp get_last_key_establishment(server, state, now) do + state + |> Map.get(server, initial_data(now)) + |> Map.get(:last_key_establishment) + end + + defp initial_data(now) do + %{ + response: {:error, "uninitialized"}, + last_key_establishment: DateTime.add(now, -@interval_in_seconds, :second) + } + end + + defp current_data(server, now) do + %{ + key_establishment_response: KeyEstablishmentClient.key_establishment(server), + last_key_establishment: now + } + end + + defp interval_surpassed?(now, last_key_establishment) do + DateTime.diff(now, last_key_establishment, :second) >= @interval_in_seconds + end +end diff --git a/lib/chronoscope/nts/key_establishment_client.ex b/lib/chronoscope/nts/key_establishment_client.ex new file mode 100644 index 0000000..d1c3cb8 --- /dev/null +++ b/lib/chronoscope/nts/key_establishment_client.ex @@ -0,0 +1,64 @@ +defmodule Chronoscope.NTS.KeyEstablishmentClient do + require Logger + + alias Chronoscope.NTS.KeyEstablishmentRequest + alias Chronoscope.NTS.KeyEstablishmentResponse + + @timeout_in_milliseconds 3000 + + def key_establishment(%{host: host, port: port}) do + case ssl_connect(host, port) do + {:ok, socket} -> perform_key_establishment(socket) + {:error, {:tls_alert, {:handshake_failure, error}}} -> {:error, to_string(error)} + {:error, :timeout} -> {:error, :timeout} + {:error, error} -> {:error, inspect(error)} + error -> {:error, inspect(error)} + end + end + + defp ssl_connect(host, port) do + :ssl.connect(host, port, tls_options(host), @timeout_in_milliseconds) + end + + defp tls_options(host) do + host + |> :tls_certificate_check.options() + |> Keyword.put(:alpn_advertised_protocols, ["ntske/1"]) + end + + defp perform_key_establishment(socket) do + :ok = :ssl.send(socket, KeyEstablishmentRequest.create()) + {:ok, peercert} = :ssl.peercert(socket) + + # fixme - don't use receive inside genserver + receive do + {:ssl, _socket, response} -> + :ssl.close(socket) + + case KeyEstablishmentResponse.parse(response) do + {:ok, x} -> {:ok, Map.put(x, :cert_expiration, certificate_expiration(peercert))} + # todo - indicate errors in server response + error -> error + end + + 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 + + defp certificate_expiration(certificate) do + {:Validity, _, {:utcTime, expiration}} = + certificate + |> X509.Certificate.from_der!() + |> X509.Certificate.validity() + + expiration + end +end diff --git a/lib/chronoscope/nts/key_establishment_request.ex b/lib/chronoscope/nts/key_establishment_request.ex new file mode 100644 index 0000000..b5f7fd5 --- /dev/null +++ b/lib/chronoscope/nts/key_establishment_request.ex @@ -0,0 +1,9 @@ +defmodule Chronoscope.NTS.KeyEstablishmentRequest do + @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 create() do + @next_protocol_negotiation <> @aead_algorithm_negotiation <> @end_of_message + end +end diff --git a/lib/chronoscope/nts/key_establishment.ex b/lib/chronoscope/nts/key_establishment_response.ex similarity index 61% rename from lib/chronoscope/nts/key_establishment.ex rename to lib/chronoscope/nts/key_establishment_response.ex index 677ba73..19941a6 100644 --- a/lib/chronoscope/nts/key_establishment.ex +++ b/lib/chronoscope/nts/key_establishment_response.ex @@ -1,10 +1,6 @@ -defmodule Chronoscope.NTS.KeyEstablishment do +defmodule Chronoscope.NTS.KeyEstablishmentResponse 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>> - @aead_alogorithms %{ 15 => "AEAD_AES_SIV_CMAC_256", 30 => "AEAD_AES_128_GCM_SIV" @@ -20,69 +16,65 @@ defmodule Chronoscope.NTS.KeyEstablishment do 2 => "Internal Server Error" } - def request() do - @next_protocol_negotiation <> @aead_algorithm_negotiation <> @end_of_message + def parse(response) do + {:ok, parse_response(response, %{})} end - def parse_response(response) do - {:ok, do_parse_response(response, %{})} - end - - defp do_parse_response([], acc) do + defp parse_response([], acc) do acc end # End of Message - defp do_parse_response([0x80, 0x00, 0x00, 0x00 | _rest], acc) do + defp parse_response([0x80, 0x00, 0x00, 0x00 | _rest], acc) do acc end # NTS Next Protocol Negotiation - defp do_parse_response([0x80, 0x01, length_high, length_low | rest], acc) do + defp 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( + parse_response( remaining, Map.put(acc, :next_protocols, parse_next_protocol_list(next_protocols)) ) end # Error - defp do_parse_response([0x80, 0x02, 0x00, 0x02, error_high, error_low | rest], acc) do + defp parse_response([0x80, 0x02, 0x00, 0x02, error_high, error_low | rest], acc) do error = combine_octets(error_high, error_low) - do_parse_response( + parse_response( rest, Map.put(acc, :error, Map.get(@errors, error, to_string(error))) ) end # Warning - defp do_parse_response([0x80, 0x03, 0x00, 0x02, warning_high, warning_low | rest], acc) do + defp parse_response([0x80, 0x03, 0x00, 0x02, warning_high, warning_low | rest], acc) do warning = combine_octets(warning_high, warning_low) - do_parse_response(rest, Map.put(acc, :warning, to_string(warning))) + parse_response(rest, Map.put(acc, :warning, to_string(warning))) end # AEAD Algorithm Negotiation - defp do_parse_response([first, 0x04, length_high, length_low | rest], acc) + defp 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( + parse_response( remaining, Map.put(acc, :aead_algorithms, parse_aead_algorithm_list(aead_algorithms)) ) end # New Cookie for NTPv4 - defp do_parse_response([first, 0x05, length_high, length_low | rest], acc) when first == 0x00 or first == 0x80 do + defp 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( + parse_response( remaining, acc |> Map.update(:cookies, [cookie], &[cookie | &1]) @@ -91,25 +83,25 @@ defmodule Chronoscope.NTS.KeyEstablishment do end # NTPv4 Server Negotiation - defp do_parse_response([first, 0x06, length_high, length_low | rest], acc) when first == 0x00 or first == 0x80 do + defp parse_response([first, 0x06, length_high, length_low | rest], acc) when first == 0x00 or first == 0x80 do length = combine_octets(length_high, length_low) {server, remaining} = Enum.split(rest, length) - do_parse_response(remaining, Map.put(acc, :server, to_string(server))) + parse_response(remaining, Map.put(acc, :server, to_string(server))) end # NTPv4 Port Negotiation - defp do_parse_response([first, 0x07, 0x00, 0x02, port_high, port_low | rest], acc) when first == 0x00 or first == 0x80 do + defp parse_response([first, 0x07, 0x00, 0x02, port_high, port_low | rest], acc) when first == 0x00 or first == 0x80 do port = combine_octets(port_high, port_low) - do_parse_response(rest, Map.put(acc, :port, port)) + parse_response(rest, Map.put(acc, :port, port)) end - defp do_parse_response([_, _, length_high, length_low | rest], acc) do + defp 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) + parse_response(remaining, acc) end defp parse_next_protocol_list(next_protocols) do diff --git a/lib/chronoscope_web/controllers/api/v1/health_controller.ex b/lib/chronoscope_web/controllers/api/v1/health_controller.ex index 8c3d7f3..1562ecd 100644 --- a/lib/chronoscope_web/controllers/api/v1/health_controller.ex +++ b/lib/chronoscope_web/controllers/api/v1/health_controller.ex @@ -1,7 +1,9 @@ defmodule ChronoscopeWeb.API.V1.HealthController do use ChronoscopeWeb, :controller + alias Chronoscope.NTS + def get(conn, _params) do - json(conn, %{healthy: true}) + json(conn, %{healthy: NTS.healthy?()}) end end diff --git a/lib/chronoscope_web/controllers/api/v1/nts/nts_client.ex b/lib/chronoscope_web/controllers/api/v1/nts/nts_client.ex deleted file mode 100644 index e69de29..0000000 diff --git a/test/chronoscope/nts/key_establishment_request_test.exs b/test/chronoscope/nts/key_establishment_request_test.exs new file mode 100644 index 0000000..39fa524 --- /dev/null +++ b/test/chronoscope/nts/key_establishment_request_test.exs @@ -0,0 +1,10 @@ +defmodule Chronoscope.NTS.KeyEstablishmentRequestTest do + use ExUnit.Case + + import Chronoscope.NTS.KeyEstablishmentRequest + + test "builds request" do + assert create() == + <<0x80, 0x01, 0x00, 0x02, 0x00, 0x00, 0x80, 0x04, 0x00, 0x04, 0x00, 0x1E, 0x00, 0x0F, 0x80, 0x00, 0x00, 0x00>> + end +end diff --git a/test/chronoscope/nts/key_establishment_response_test.exs b/test/chronoscope/nts/key_establishment_response_test.exs new file mode 100644 index 0000000..0421629 --- /dev/null +++ b/test/chronoscope/nts/key_establishment_response_test.exs @@ -0,0 +1,144 @@ +defmodule Chronoscope.NTS.KeyEstablishmentResponseTest do + use ExUnit.Case + + import Chronoscope.NTS.KeyEstablishmentResponse + + test "handles empty response" do + assert parse([]) == {:ok, %{}} + end + + test "ignores unkown record" do + assert parse([0x80, 0x21, 0x00, 0x04, 0x00, 0x01, 0x02, 0x03]) == {:ok, %{}} + end + + test "handles end of message record" do + assert parse([0x80, 0x00, 0x00, 0x00]) == {:ok, %{}} + end + + test "handles next protocol negotiation record" do + assert parse([0x80, 0x01, 0x00, 0x02, 0x00, 0x00]) == {:ok, %{next_protocols: ["NTPv4"]}} + end + + test "does not handle next protocol negotiation record without critical bit" do + assert parse([0x00, 0x01, 0x00, 0x02, 0x00, 0x00]) == {:ok, %{}} + end + + test "handles empty next protocols" do + assert parse([0x80, 0x01, 0x00, 0x00]) == {:ok, %{next_protocols: []}} + end + + test "handles multiple next protocols" do + assert parse([0x80, 0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]) == + {:ok, %{next_protocols: ["NTPv4", "UNASSIGNED", "NTPv4"]}} + end + + test "handles aead algorithm negotiation record" do + assert parse([0x80, 0x04, 0x00, 0x02, 0x00, 0x0F]) == {:ok, %{aead_algorithms: ["AEAD_AES_SIV_CMAC_256"]}} + end + + test "handles aead algorithm negotiation record without critical bit" do + assert parse([0x00, 0x04, 0x00, 0x02, 0x00, 0x1E]) == {:ok, %{aead_algorithms: ["AEAD_AES_128_GCM_SIV"]}} + end + + test "handles empty aead algorithms" do + assert parse([0x80, 0x04, 0x00, 0x00]) == {:ok, %{aead_algorithms: []}} + end + + test "handles multiple aead algorithms" do + assert parse([0x80, 0x04, 0x00, 0x06, 0x00, 0x1E, 0x00, 0x01, 0x00, 0x0F]) == + {:ok, %{aead_algorithms: ["AEAD_AES_SIV_CMAC_256", "UNKNOWN", "AEAD_AES_128_GCM_SIV"]}} + end + + test "handles error record" do + assert parse([0x80, 0x02, 0x00, 0x02, 0x00, 0x01]) == {:ok, %{error: "Bad Request"}} + end + + test "handles unknown error record" do + assert parse([0x80, 0x02, 0x00, 0x02, 0x00, 0x99]) == {:ok, %{error: "153"}} + end + + test "does not handle error record without critical bit" do + assert parse([0x00, 0x02, 0x00, 0x02, 0x00, 0x01]) == {:ok, %{}} + end + + test "handles warning record" do + assert parse([0x80, 0x03, 0x00, 0x02, 0x00, 0x01]) == {:ok, %{warning: "1"}} + end + + test "does not handle warning record without critical bit" do + assert parse([0x00, 0x03, 0x00, 0x02, 0x00, 0x01]) == {:ok, %{}} + end + + test "handles server record" do + assert parse([0x80, 0x06, 0x00, 0x09, ?1, ?2, ?7, ?., ?0, ?., ?0, ?., ?1]) == {:ok, %{server: "127.0.0.1"}} + end + + test "handles server record without critical bit" do + assert parse([0x00, 0x06, 0x00, 0x09, ?1, ?2, ?7, ?., ?0, ?., ?0, ?., ?1]) == {:ok, %{server: "127.0.0.1"}} + end + + test "handles port record" do + assert parse([0x80, 0x07, 0x00, 0x02, 0x04, 0xCE]) == {:ok, %{port: 1230}} + end + + test "handles port record without critical bit" do + assert parse([0x00, 0x07, 0x00, 0x02, 0x04, 0xCE]) == {:ok, %{port: 1230}} + end + + test "handles cookie record" do + assert parse([0x80, 0x05, 0x00, 0x0E, ?c, ?h, ?o, ?c, ?o, ?l, ?a, ?t, ?e, ?_, ?c, ?h, ?i, ?p]) == + {:ok, %{cookies: [~c"chocolate_chip"], cookie_length: 14}} + end + + test "handles cookie record without critical bit" do + assert parse([0x00, 0x05, 0x00, 0x0E, ?c, ?h, ?o, ?c, ?o, ?l, ?a, ?t, ?e, ?_, ?c, ?h, ?i, ?p]) == + {:ok, %{cookies: [~c"chocolate_chip"], cookie_length: 14}} + end + + test "sets cookie length to longest cookie" do + assert parse( + [0x80, 0x05, 0x00, 0x01, ?c] ++ + [0x80, 0x05, 0x00, 0x03, ?c, ?c, ?c] ++ + [0x80, 0x05, 0x00, 0x01, ?c] + ) == + {:ok, %{cookies: [~c"c", ~c"ccc", ~c"c"], cookie_length: 3}} + end + + test "handles full response" do + assert parse( + [0x80, 0x01, 0x00, 0x02, 0x00, 0x00] ++ + [0x80, 0x04, 0x00, 0x06, 0x00, 0x1E, 0x00, 0x01, 0x00, 0x0F] ++ + [0x80, 0x05, 0x00, 0x01, ?c] ++ + [0x00, 0x21, 0x00, 0x04, 0x00, 0x01, 0x02, 0x03] ++ + [0x80, 0x06, 0x00, 0x09, ?1, ?2, ?7, ?., ?0, ?., ?0, ?., ?1] ++ + [0x80, 0x07, 0x00, 0x02, 0x04, 0xCE] ++ + [0x80, 0x00, 0x00, 0x00] + ) == + {:ok, + %{ + cookies: [~c"c"], + aead_algorithms: ["AEAD_AES_SIV_CMAC_256", "UNKNOWN", "AEAD_AES_128_GCM_SIV"], + cookie_length: 1, + next_protocols: ["NTPv4"], + port: 1230, + server: "127.0.0.1" + }} + end + + test "doesn't read past end of message record" do + assert parse( + [0x80, 0x01, 0x00, 0x02, 0x00, 0x00] ++ + [0x00, 0x21, 0x00, 0x04, 0x00, 0x01, 0x02, 0x03] ++ + [0x80, 0x04, 0x00, 0x02, 0x00, 0x1E] ++ + [0x80, 0x00, 0x00, 0x00] ++ + [0x80, 0x05, 0x00, 0x01, ?c] ++ + [0x80, 0x06, 0x00, 0x09, ?1, ?2, ?7, ?., ?0, ?., ?0, ?., ?1] ++ + [0x80, 0x07, 0x00, 0x02, 0x04, 0xCE] + ) == + {:ok, + %{ + aead_algorithms: ["AEAD_AES_128_GCM_SIV"], + next_protocols: ["NTPv4"] + }} + end +end diff --git a/test/chronoscope/nts/key_establishment_test.exs b/test/chronoscope/nts/key_establishment_test.exs deleted file mode 100644 index c1e126a..0000000 --- a/test/chronoscope/nts/key_establishment_test.exs +++ /dev/null @@ -1,153 +0,0 @@ -defmodule Chronoscope.NTS.KeyEstablishmentTest do - use ExUnit.Case - - alias Chronoscope.NTS.KeyEstablishment - - test "builds request" do - assert KeyEstablishment.request() == - <<0x80, 0x01, 0x00, 0x02, 0x00, 0x00, 0x80, 0x04, 0x00, 0x04, 0x00, 0x1E, 0x00, 0x0F, 0x80, 0x00, 0x00, 0x00>> - end - - test "handles empty response" do - assert KeyEstablishment.parse_response([]) == {:ok, %{}} - end - - test "ignores unkown record" do - assert KeyEstablishment.parse_response([0x80, 0x21, 0x00, 0x04, 0x00, 0x01, 0x02, 0x03]) == {:ok, %{}} - end - - test "handles end of message record" do - assert KeyEstablishment.parse_response([0x80, 0x00, 0x00, 0x00]) == {:ok, %{}} - end - - test "handles next protocol negotiation record" do - assert KeyEstablishment.parse_response([0x80, 0x01, 0x00, 0x02, 0x00, 0x00]) == {:ok, %{next_protocols: ["NTPv4"]}} - end - - test "does not handle next protocol negotiation record without critical bit" do - assert KeyEstablishment.parse_response([0x00, 0x01, 0x00, 0x02, 0x00, 0x00]) == {:ok, %{}} - end - - test "handles empty next protocols" do - assert KeyEstablishment.parse_response([0x80, 0x01, 0x00, 0x00]) == {:ok, %{next_protocols: []}} - end - - test "handles multiple next protocols" do - assert KeyEstablishment.parse_response([0x80, 0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]) == - {:ok, %{next_protocols: ["NTPv4", "UNASSIGNED", "NTPv4"]}} - end - - test "handles aead algorithm negotiation record" do - assert KeyEstablishment.parse_response([0x80, 0x04, 0x00, 0x02, 0x00, 0x0F]) == - {:ok, %{aead_algorithms: ["AEAD_AES_SIV_CMAC_256"]}} - end - - test "handles aead algorithm negotiation record without critical bit" do - assert KeyEstablishment.parse_response([0x00, 0x04, 0x00, 0x02, 0x00, 0x1E]) == - {:ok, %{aead_algorithms: ["AEAD_AES_128_GCM_SIV"]}} - end - - test "handles empty aead algorithms" do - assert KeyEstablishment.parse_response([0x80, 0x04, 0x00, 0x00]) == {:ok, %{aead_algorithms: []}} - end - - test "handles multiple aead algorithms" do - assert KeyEstablishment.parse_response([0x80, 0x04, 0x00, 0x06, 0x00, 0x1E, 0x00, 0x01, 0x00, 0x0F]) == - {:ok, %{aead_algorithms: ["AEAD_AES_SIV_CMAC_256", "UNKNOWN", "AEAD_AES_128_GCM_SIV"]}} - end - - test "handles error record" do - assert KeyEstablishment.parse_response([0x80, 0x02, 0x00, 0x02, 0x00, 0x01]) == {:ok, %{error: "Bad Request"}} - end - - test "handles unknown error record" do - assert KeyEstablishment.parse_response([0x80, 0x02, 0x00, 0x02, 0x00, 0x99]) == {:ok, %{error: "153"}} - end - - test "does not handle error record without critical bit" do - assert KeyEstablishment.parse_response([0x00, 0x02, 0x00, 0x02, 0x00, 0x01]) == {:ok, %{}} - end - - test "handles warning record" do - assert KeyEstablishment.parse_response([0x80, 0x03, 0x00, 0x02, 0x00, 0x01]) == {:ok, %{warning: "1"}} - end - - test "does not handle warning record without critical bit" do - assert KeyEstablishment.parse_response([0x00, 0x03, 0x00, 0x02, 0x00, 0x01]) == {:ok, %{}} - end - - test "handles server record" do - assert KeyEstablishment.parse_response([0x80, 0x06, 0x00, 0x09, ?1, ?2, ?7, ?., ?0, ?., ?0, ?., ?1]) == - {:ok, %{server: "127.0.0.1"}} - end - - test "handles server record without critical bit" do - assert KeyEstablishment.parse_response([0x00, 0x06, 0x00, 0x09, ?1, ?2, ?7, ?., ?0, ?., ?0, ?., ?1]) == - {:ok, %{server: "127.0.0.1"}} - end - - test "handles port record" do - assert KeyEstablishment.parse_response([0x80, 0x07, 0x00, 0x02, 0x04, 0xCE]) == {:ok, %{port: 1230}} - end - - test "handles port record without critical bit" do - assert KeyEstablishment.parse_response([0x00, 0x07, 0x00, 0x02, 0x04, 0xCE]) == {:ok, %{port: 1230}} - end - - test "handles cookie record" do - assert KeyEstablishment.parse_response([0x80, 0x05, 0x00, 0x0E, ?c, ?h, ?o, ?c, ?o, ?l, ?a, ?t, ?e, ?_, ?c, ?h, ?i, ?p]) == - {:ok, %{cookies: ['chocolate_chip'], cookie_length: 14}} - end - - test "handles cookie record without critical bit" do - assert KeyEstablishment.parse_response([0x00, 0x05, 0x00, 0x0E, ?c, ?h, ?o, ?c, ?o, ?l, ?a, ?t, ?e, ?_, ?c, ?h, ?i, ?p]) == - {:ok, %{cookies: ['chocolate_chip'], cookie_length: 14}} - end - - test "sets cookie length to longest cookie" do - assert KeyEstablishment.parse_response( - [0x80, 0x05, 0x00, 0x01, ?c] ++ - [0x80, 0x05, 0x00, 0x03, ?c, ?c, ?c] ++ - [0x80, 0x05, 0x00, 0x01, ?c] - ) == - {:ok, %{cookies: ['c', 'ccc', 'c'], cookie_length: 3}} - end - - test "handles full response" do - assert KeyEstablishment.parse_response( - [0x80, 0x01, 0x00, 0x02, 0x00, 0x00] ++ - [0x80, 0x04, 0x00, 0x06, 0x00, 0x1E, 0x00, 0x01, 0x00, 0x0F] ++ - [0x80, 0x05, 0x00, 0x01, ?c] ++ - [0x00, 0x21, 0x00, 0x04, 0x00, 0x01, 0x02, 0x03] ++ - [0x80, 0x06, 0x00, 0x09, ?1, ?2, ?7, ?., ?0, ?., ?0, ?., ?1] ++ - [0x80, 0x07, 0x00, 0x02, 0x04, 0xCE] ++ - [0x80, 0x00, 0x00, 0x00] - ) == - {:ok, - %{ - cookies: ['c'], - aead_algorithms: ["AEAD_AES_SIV_CMAC_256", "UNKNOWN", "AEAD_AES_128_GCM_SIV"], - cookie_length: 1, - next_protocols: ["NTPv4"], - port: 1230, - server: "127.0.0.1" - }} - end - - test "doesn't read past end of message record" do - assert KeyEstablishment.parse_response( - [0x80, 0x01, 0x00, 0x02, 0x00, 0x00] ++ - [0x00, 0x21, 0x00, 0x04, 0x00, 0x01, 0x02, 0x03] ++ - [0x80, 0x04, 0x00, 0x02, 0x00, 0x1E] ++ - [0x80, 0x00, 0x00, 0x00] ++ - [0x80, 0x05, 0x00, 0x01, ?c] ++ - [0x80, 0x06, 0x00, 0x09, ?1, ?2, ?7, ?., ?0, ?., ?0, ?., ?1] ++ - [0x80, 0x07, 0x00, 0x02, 0x04, 0xCE] - ) == - {:ok, - %{ - aead_algorithms: ["AEAD_AES_128_GCM_SIV"], - next_protocols: ["NTPv4"] - }} - end -end