Parse NTS-KE response

This commit is contained in:
Mike Cifelli 2024-03-16 15:34:13 -04:00
parent ab8a31bad4
commit 68c1952383
Signed by: mike
GPG Key ID: 6B08C6BE47D08E4C
5 changed files with 131 additions and 44 deletions

View File

@ -1,3 +1,2 @@
defmodule Chronoscope.Gemini do defmodule Chronoscope.Gemini do
end end

View File

@ -1,3 +1,38 @@
defmodule Chronoscope.NTS do defmodule Chronoscope.NTS do
require Logger
alias Chronoscope.NTS.KeyEstablishment
@timeout_in_milliseconds 3000
def key_establishment(host, port) do
tls_options = :tls_certificate_check.options(host) ++ [alpn_advertised_protocols: ["ntske/1"]]
case :ssl.connect(host, port, tls_options, @timeout_in_milliseconds) do
{:ok, socket} -> perform_key_establishment(socket)
{:error, {:tls_alert, {:handshake_failure, error}}} -> {:error, to_string(error)}
{:error, error} -> {:error, inspect(error)}
error -> {:error, inspect(error)}
end
end
defp perform_key_establishment(socket) do
:ok = :ssl.send(socket, KeyEstablishment.request())
receive do
{:ssl, _socket, response} ->
:ssl.close(socket)
KeyEstablishment.parse_response(response)
msg ->
:ssl.close(socket)
Logger.error("received unexpected message: #{inspect(msg)}")
{:error, :no_response}
after
@timeout_in_milliseconds ->
:ssl.close(socket)
Logger.error("timed out waiting for response")
{:error, :timeout}
end
end
end end

View File

@ -0,0 +1,90 @@
defmodule Chronoscope.NTS.KeyEstablishment do
import Bitwise
@next_protocol_negotiation <<0x80, 0x01, 0x00, 0x02, 0x00, 0x00>>
@aead_algorithm_negotiation <<0x80, 0x04, 0x00, 0x04, 0x00, 0x1E, 0x00, 0x0F>>
@end_of_message <<0x80, 0x00, 0x00, 0x00>>
def request() do
@next_protocol_negotiation <> @aead_algorithm_negotiation <> @end_of_message
end
def parse_response(response) do
{:ok, Map.update(do_parse_response(response, %{}), :cookies, 0, &length/1)}
end
defp do_parse_response([], acc) do
acc
end
defp do_parse_response([0x80, 0x00, 0x00, 0x00], acc) do
acc
end
defp do_parse_response([0x80, 0x01, length_high, length_low | rest], acc) do
length = combine_octets(length_high, length_low)
{next_protocols, remaining} = Enum.split(rest, length)
do_parse_response(
remaining,
Map.put(acc, :next_protocols, parse_next_protocol(next_protocols))
)
end
defp do_parse_response([first, 0x04, length_high, length_low | rest], acc)
when first == 0x00 or first == 0x80 do
length = combine_octets(length_high, length_low)
{aead_algorithms, remaining} = Enum.split(rest, length)
do_parse_response(
remaining,
Map.put(acc, :aead_algorithms, parse_aead_algorithm(aead_algorithms))
)
end
defp do_parse_response([first, 0x05, length_high, length_low | rest], acc)
when first == 0x00 or first == 0x80 do
length = combine_octets(length_high, length_low)
{cookie, remaining} = Enum.split(rest, length)
do_parse_response(
remaining,
acc
|> Map.update(:cookies, [cookie], &[cookie | &1])
|> Map.put(:cookie_length, length)
)
end
defp do_parse_response([_, _, length_high, length_low | rest], acc) do
length = combine_octets(length_high, length_low)
{_, remaining} = Enum.split(rest, length)
do_parse_response(remaining, acc)
end
defp parse_aead_algorithm([0x00, 0x0F]) do
"AEAD_AES_SIV_CMAC_256"
end
defp parse_aead_algorithm([0x00, 0x1E]) do
"AEAD_AES_128_GCM_SIV"
end
defp parse_aead_algorithm(_aead_algorithm) do
"UNKNOWN"
end
defp parse_next_protocol([0x00, 0x00]) do
"NTPv4"
end
defp parse_next_protocol(_next_protocol) do
"UNASSIGNED"
end
# todo parse server/port information
defp combine_octets(high, low) do
high <<< 8 ||| low
end
end

View File

@ -1,2 +0,0 @@
defmodule Chronoscope.NTS.KeyExchange do
end

View File

@ -3,58 +3,23 @@ defmodule ChronoscopeWeb.API.NTS.KeyExchangeController do
require Logger require Logger
alias Chronoscope.NTS
@default_port "4460" @default_port "4460"
@timeout_in_milliseconds 3000
def get(conn, params) do def get(conn, params) do
host = to_charlist(params["host"]) host = to_charlist(params["host"])
port = String.to_integer(params["port"] || @default_port) port = String.to_integer(params["port"] || @default_port)
tls_options = :tls_certificate_check.options(host) ++ [alpn_advertised_protocols: ["ntse/1"]] case NTS.key_establishment(host, port) do
{:ok, configuration} ->
case :ssl.connect(host, port, tls_options, @timeout_in_milliseconds) do ok_response(conn, %{status: :ok, configuration: configuration})
{:ok, socket} ->
key_exchange_request =
<<0x80, 0x01, 0x00, 0x02, 0x00, 0x00, 0x80, 0x04, 0x00, 0x04, 0x00, 0x1E, 0x00, 0x0F,
0x80, 0x00, 0x00, 0x00>>
:ok = :ssl.send(socket, key_exchange_request)
receive do
{:ssl, _socket, data} ->
:ssl.close(socket)
ok_response(conn, %{status: :ok, response: parse_response(data)})
msg ->
:ssl.close(socket)
Logger.error("received unexpected message: #{inspect(msg)}")
ok_response(conn, %{status: :error, reason: :no_response})
after
@timeout_in_milliseconds ->
:ssl.close(socket)
Logger.error("timed out waiting for response")
ok_response(conn, %{status: :error, reason: :timeout})
end
{:error, {:tls_alert, {:handshake_failure, error}}} ->
ok_response(conn, %{status: :error, reason: to_string(error)})
{:error, error} -> {:error, error} ->
ok_response(conn, %{status: :error, reason: inspect(error)}) ok_response(conn, %{status: :error, reason: inspect(error)})
error ->
ok_response(conn, %{status: :error, reason: inspect(error)})
end end
end end
defp parse_response([0x80 | _] = _data) do
"success"
end
defp parse_response(_data) do
"failure"
end
defp ok_response(conn, body) do defp ok_response(conn, body) do
conn conn
|> Plug.Conn.put_resp_content_type("application/json") |> Plug.Conn.put_resp_content_type("application/json")