How to Dynamically Generate an SSH Host Key for :ssh.daemon/2,3 in Erlang/Elixir?

Hi everyone,

I’m trying to start an SSH daemon in Elixir using :ssh.daemon/2,3, and I want to dynamically generate an SSH host key instead of relying on a pre-existing key file.

I initially tried to do in elixir

defmodule MyWeb.SFTPServer do

  def start do
    port = 8989

    sftp_dir = Path.join(System.tmp_dir!(), "sftp_dir")
    File.mkdir(sftp_dir)

    opts = [
      {:key_cb, {KeyCallback, []}},
      {:system_dir, to_charlist("/tmp")},
      {:user_passwords, [{~c"user", ~c"password"}]},
      {:subsystems, [:ssh_sftpd.subsystem_spec([{:root, to_charlist(sftp_dir)}])]}
    ]

    :ssh.daemon(port, opts)
  end
end

defmodule KeyCallback do
  @moduledoc false
  @behaviour :ssh_server_key_api

  @impl :ssh_server_key_api
  def host_key(:rsa, _damon_options) do
    {private_key, _} = generate_key()
    IO.inspect(private_key)
    {:ok, private_key}
  end

  @impl :ssh_server_key_api
  def is_auth_key(_publicKey, _user, _daemon_options) do
    true
  end

  defp generate_key do
    # http://erlang.org/doc/apps/public_key/public_key_records.html#rsa

    {:RSAPrivateKey, _version, modulus, public_exponent, _private_exponent, _prime1, _prime2,
     _exponent1, _exponent2, _coefficient,
     _other_primeInfos} = rsa_private_key = :public_key.generate_key({:rsa, 2048, 65537})

    rsa_public_key = {:RSAPublicKey, modulus, public_exponent}

    rsa_private_key_pem_entry = :public_key.pem_entry_encode(:RSAPrivateKey, rsa_private_key)
    rsa_public_key_pem_entry = :public_key.pem_entry_encode(:RSAPublicKey, rsa_public_key)

    pem_out_public = :public_key.pem_encode([rsa_public_key_pem_entry])
    pem_out_private = :public_key.pem_encode([rsa_private_key_pem_entry])

    {pem_out_private, pem_out_public}
  end
end

However, I am unsure how to pass the generated key to :ssh.daemon/2,3 as an option.

  1. Am I generating the key correctly for use with :ssh.daemon/2,3?
  2. How can I pass the generated key to :ssh.daemon/2,3 so that it does not require a file-based key?

I appreciate any guidance on this!

def start do
    port = 8989
    sftp_dir = Path.join(System.tmp_dir!(), "sftp_dir")
    File.mkdir(sftp_dir)

    opts = [
      {:key_cb, {MyWeb.SFTPServer.KeyCallback, []}},
      {:system_dir, to_charlist("/tmp")},
      {:user_passwords, [{~c"user", ~c"password"}]},
      {:subsystems, [:ssh_sftpd.subsystem_spec([{:root, to_charlist(sftp_dir)}])]}
    ]

    :ssh.daemon(port, opts)
  end

  defmodule KeyCallback do
    @behaviour :ssh_server_key_api

    @impl true
    def host_key(:"rsa-sha2-512", _damon_options) do
      {:ok, :public_key.generate_key({:rsa, 2048, 65_537})}
    end

    @impl true
    def host_key(_, _) do
      {:error, :unsupported}
    end

    @impl true
    def is_auth_key(_publicKey, _user, _daemon_options), do: true
1 Like