diff --git a/lib/chronoscope/nts/certificate.ex b/lib/chronoscope/nts/certificate.ex new file mode 100644 index 0000000..fae7a48 --- /dev/null +++ b/lib/chronoscope/nts/certificate.ex @@ -0,0 +1,35 @@ +defmodule Chronoscope.NTS.Certificate do + def expiration_date(certificate) do + {:Validity, _, {:utcTime, expiration}} = + certificate + |> X509.Certificate.from_der!() + |> X509.Certificate.validity() + + cert_time_to_iso8601(expiration) + end + + def cert_time_to_iso8601(cert_time) do + captures = + Regex.named_captures( + ~r/^(?\d\d)(?\d\d)(?\d\d)(?\d\d)(?\d\d)(?\d\d)Z$/, + to_string(cert_time) + ) + + "#{short_year_to_full_year(captures["year"])}-#{captures["month"]}-#{captures["day"]}T#{captures["hour"]}:#{captures["minute"]}:#{captures["second"]}Z" + end + + defp short_year_to_full_year(short_year) do + {century, current_year} = + DateTime.utc_now().year + |> to_string() + |> String.split_at(-2) + + if String.to_integer(short_year) >= String.to_integer(current_year) do + century <> short_year + else + century + |> String.to_integer() + |> then(&"#{&1 + 1}#{short_year}") + end + end +end diff --git a/lib/chronoscope/nts/key_establishment_client.ex b/lib/chronoscope/nts/key_establishment_client.ex index d1c3cb8..aa5d996 100644 --- a/lib/chronoscope/nts/key_establishment_client.ex +++ b/lib/chronoscope/nts/key_establishment_client.ex @@ -1,6 +1,7 @@ defmodule Chronoscope.NTS.KeyEstablishmentClient do require Logger + alias Chronoscope.NTS.Certificate alias Chronoscope.NTS.KeyEstablishmentRequest alias Chronoscope.NTS.KeyEstablishmentResponse @@ -36,7 +37,7 @@ defmodule Chronoscope.NTS.KeyEstablishmentClient do :ssl.close(socket) case KeyEstablishmentResponse.parse(response) do - {:ok, x} -> {:ok, Map.put(x, :cert_expiration, certificate_expiration(peercert))} + {:ok, parsed_response} -> {:ok, Map.put(parsed_response, :cert_expiration, Certificate.expiration_date(peercert))} # todo - indicate errors in server response error -> error end @@ -52,13 +53,4 @@ defmodule Chronoscope.NTS.KeyEstablishmentClient do {: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_web/controllers/api/v1/nts/key_establishment_controller.ex b/lib/chronoscope_web/controllers/api/v1/nts/key_establishment_controller.ex index a960047..107954d 100644 --- a/lib/chronoscope_web/controllers/api/v1/nts/key_establishment_controller.ex +++ b/lib/chronoscope_web/controllers/api/v1/nts/key_establishment_controller.ex @@ -22,7 +22,7 @@ defmodule ChronoscopeWeb.API.V1.NTS.KeyEstablishmentController do defp format_response(response) do response - |> Map.take([:aead_algorithms, :cookie_length, :cookies, :next_protocols, :port, :server]) + |> Map.take([:aead_algorithms, :cert_expiration, :cookie_length, :cookies, :next_protocols, :port, :server]) |> Map.update(:cookies, 0, &length/1) end end diff --git a/test/chronoscope/nts/certificate_test.exs b/test/chronoscope/nts/certificate_test.exs new file mode 100644 index 0000000..ea63d7c --- /dev/null +++ b/test/chronoscope/nts/certificate_test.exs @@ -0,0 +1,26 @@ +defmodule Chronoscope.NTS.CertificateTest do + use ExUnit.Case + + import Chronoscope.NTS.Certificate + + test "parses the expiration date of a certificate" do + {:ok, expiration, _} = + :secp256r1 + |> X509.PrivateKey.new_ec() + |> X509.Certificate.self_signed("/C=US/ST=CA/L=San Francisco/O=Acme/CN=Test", validity: 12) + |> X509.Certificate.to_der() + |> expiration_date() + |> DateTime.from_iso8601() + + assert DateTime.diff(expiration, DateTime.utc_now(), :day) == 12 + end + + # todo - mock current time + test "converts certificate datetime to iso8601" do + assert cert_time_to_iso8601("240326110000Z") == "2024-03-26T11:00:00Z" + end + + test "handles century rollover" do + assert cert_time_to_iso8601("010326110000Z") == "2101-03-26T11:00:00Z" + end +end