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)
|
# Start a worker by calling: Chronoscope.Worker.start_link(arg)
|
||||||
# {Chronoscope.Worker, arg},
|
# {Chronoscope.Worker, arg},
|
||||||
# Start to serve requests, typically the last entry
|
# Start to serve requests, typically the last entry
|
||||||
ChronoscopeWeb.Endpoint
|
ChronoscopeWeb.Endpoint,
|
||||||
|
Chronoscope.NTS.Client
|
||||||
]
|
]
|
||||||
|
|
||||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||||
|
|
|
@ -7,59 +7,17 @@ defmodule Chronoscope.NTS do
|
||||||
|
|
||||||
require Logger
|
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
|
def key_establishment(host, port) do
|
||||||
case ssl_connect(host, port) do
|
GenServer.call(Client, {:key_establishment, %{host: host, port: port}})
|
||||||
{: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
|
|
||||||
end
|
end
|
||||||
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
|
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 %{
|
@aead_alogorithms %{
|
||||||
15 => "AEAD_AES_SIV_CMAC_256",
|
15 => "AEAD_AES_SIV_CMAC_256",
|
||||||
30 => "AEAD_AES_128_GCM_SIV"
|
30 => "AEAD_AES_128_GCM_SIV"
|
||||||
|
@ -20,69 +16,65 @@ defmodule Chronoscope.NTS.KeyEstablishment do
|
||||||
2 => "Internal Server Error"
|
2 => "Internal Server Error"
|
||||||
}
|
}
|
||||||
|
|
||||||
def request() do
|
def parse(response) do
|
||||||
@next_protocol_negotiation <> @aead_algorithm_negotiation <> @end_of_message
|
{:ok, parse_response(response, %{})}
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_response(response) do
|
defp parse_response([], acc) do
|
||||||
{:ok, do_parse_response(response, %{})}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp do_parse_response([], acc) do
|
|
||||||
acc
|
acc
|
||||||
end
|
end
|
||||||
|
|
||||||
# End of Message
|
# 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
|
acc
|
||||||
end
|
end
|
||||||
|
|
||||||
# NTS Next Protocol Negotiation
|
# 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)
|
length = combine_octets(length_high, length_low)
|
||||||
{next_protocols, remaining} = Enum.split(rest, length)
|
{next_protocols, remaining} = Enum.split(rest, length)
|
||||||
|
|
||||||
do_parse_response(
|
parse_response(
|
||||||
remaining,
|
remaining,
|
||||||
Map.put(acc, :next_protocols, parse_next_protocol_list(next_protocols))
|
Map.put(acc, :next_protocols, parse_next_protocol_list(next_protocols))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Error
|
# 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)
|
error = combine_octets(error_high, error_low)
|
||||||
|
|
||||||
do_parse_response(
|
parse_response(
|
||||||
rest,
|
rest,
|
||||||
Map.put(acc, :error, Map.get(@errors, error, to_string(error)))
|
Map.put(acc, :error, Map.get(@errors, error, to_string(error)))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Warning
|
# 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)
|
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
|
end
|
||||||
|
|
||||||
# AEAD Algorithm Negotiation
|
# 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
|
when first == 0x00 or first == 0x80 do
|
||||||
length = combine_octets(length_high, length_low)
|
length = combine_octets(length_high, length_low)
|
||||||
{aead_algorithms, remaining} = Enum.split(rest, length)
|
{aead_algorithms, remaining} = Enum.split(rest, length)
|
||||||
|
|
||||||
do_parse_response(
|
parse_response(
|
||||||
remaining,
|
remaining,
|
||||||
Map.put(acc, :aead_algorithms, parse_aead_algorithm_list(aead_algorithms))
|
Map.put(acc, :aead_algorithms, parse_aead_algorithm_list(aead_algorithms))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
# New Cookie for NTPv4
|
# 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)
|
length = combine_octets(length_high, length_low)
|
||||||
{cookie, remaining} = Enum.split(rest, length)
|
{cookie, remaining} = Enum.split(rest, length)
|
||||||
|
|
||||||
do_parse_response(
|
parse_response(
|
||||||
remaining,
|
remaining,
|
||||||
acc
|
acc
|
||||||
|> Map.update(:cookies, [cookie], &[cookie | &1])
|
|> Map.update(:cookies, [cookie], &[cookie | &1])
|
||||||
|
@ -91,25 +83,25 @@ defmodule Chronoscope.NTS.KeyEstablishment do
|
||||||
end
|
end
|
||||||
|
|
||||||
# NTPv4 Server Negotiation
|
# 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)
|
length = combine_octets(length_high, length_low)
|
||||||
{server, remaining} = Enum.split(rest, length)
|
{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
|
end
|
||||||
|
|
||||||
# NTPv4 Port Negotiation
|
# 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)
|
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
|
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)
|
length = combine_octets(length_high, length_low)
|
||||||
{_, remaining} = Enum.split(rest, length)
|
{_, remaining} = Enum.split(rest, length)
|
||||||
|
|
||||||
do_parse_response(remaining, acc)
|
parse_response(remaining, acc)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp parse_next_protocol_list(next_protocols) do
|
defp parse_next_protocol_list(next_protocols) do
|
|
@ -1,7 +1,9 @@
|
||||||
defmodule ChronoscopeWeb.API.V1.HealthController do
|
defmodule ChronoscopeWeb.API.V1.HealthController do
|
||||||
use ChronoscopeWeb, :controller
|
use ChronoscopeWeb, :controller
|
||||||
|
|
||||||
|
alias Chronoscope.NTS
|
||||||
|
|
||||||
def get(conn, _params) do
|
def get(conn, _params) do
|
||||||
json(conn, %{healthy: true})
|
json(conn, %{healthy: NTS.healthy?()})
|
||||||
end
|
end
|
||||||
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