lib/nerves_hub_link/configurators/nerves_key.ex

if Code.ensure_loaded?(NervesKey) do
  defmodule NervesHubLink.Configurator.NervesKey do
    @behaviour NervesHubLink.Configurator

    alias NervesHubLink.{Certificate, Configurator.Config}

    @impl NervesHubLink.Configurator
    def build(%Config{} = config) do
      # Because :nerves_key is an optional dep, it does not get added to
      # .app resource files or may be started in the wrong order
      # causing failures when calling code that is not yet started.
      # So we explicitly tell the :nerves_key to start here to ensure
      # its available when needed. This can be removed once the fix
      # has been released
      #
      # See https://github.com/erlang/otp/pull/2675
      _ = Application.ensure_all_started(:nerves_key)

      nk_opts = config.nerves_key || []

      bus_num = nk_opts[:i2c_bus] || 1
      certificate_pair = nk_opts[:certificate_pair] || :primary

      ssl =
        config.ssl
        |> maybe_add_cert(bus_num, certificate_pair)
        |> maybe_add_signer_cert(bus_num, certificate_pair)
        |> maybe_add_key(bus_num)
        |> maybe_add_sni(config)

      %{config | ssl: ssl}
    end

    defp maybe_add_cert(ssl, bus_num, certificate_pair) do
      Keyword.put_new_lazy(ssl, :cert, fn ->
        {:ok, i2c} = ATECC508A.Transport.I2C.init(bus_name: "i2c-#{bus_num}")

        NervesKey.device_cert(i2c, certificate_pair)
        |> X509.Certificate.to_der()
      end)
    end

    defp maybe_add_key(ssl, bus_num) do
      Keyword.put_new_lazy(ssl, :key, fn ->
        {:ok, engine} = NervesKey.PKCS11.load_engine()
        NervesKey.PKCS11.private_key(engine, i2c: bus_num)
      end)
    end

    defp maybe_add_signer_cert(ssl, bus_num, certificate_pair) do
      Keyword.put_new_lazy(ssl, :cacerts, fn ->
        {:ok, i2c} = ATECC508A.Transport.I2C.init(bus_name: "i2c-#{bus_num}")

        signer_cert =
          NervesKey.signer_cert(i2c, certificate_pair)
          |> X509.Certificate.to_der()

        [signer_cert | Certificate.ca_certs()]
      end)
    end

    defp maybe_add_sni(ssl, %{device_api_sni: sni}) do
      Keyword.put_new(ssl, :server_name_indication, to_charlist(sni))
    end
  end
end