Cache NTS key exchange responses

This commit is contained in:
Mike Cifelli 2024-03-23 12:19:13 -04:00
parent c90881be50
commit ac3b7ebcc7
Signed by: mike
GPG Key ID: 6B08C6BE47D08E4C
11 changed files with 342 additions and 235 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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