Cache NTS key exchange responses
This commit is contained in:
parent
c90881be50
commit
ac3b7ebcc7
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue