Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181defmodule GitSync.Ssh do @moduledoc """ Push credentials for forges that speak git over SSH.
The key lives in an `ssh-agent` started for the invocation and reached through a socket in a private directory: it reaches `ssh-add` through the environment rather than a file, so it never lands on disk. The host is verified against a private `known_hosts` holding whatever `ssh-keyscan` reported for the knot being pushed to. """
alias GitSync.Connection
@add ~s(printf '%s' "$GIT_SYNC_SSH_KEY" | ssh-add -)
@timeout "5"
@options [ "-F /dev/null", "-o StrictHostKeyChecking=yes", "-o GlobalKnownHostsFile=/dev/null", "-o IdentityFile=/dev/null" ]
@doc """ Runs `fun` with the environment that authenticates one git invocation against `connection`, verifying the host against `host_key`, and tears the agent down afterwards.
`fun` takes the environment and returns a `{status, output}` pair; the agent's paths are scrubbed from the output so they cannot reach a run log. A connection with no key runs the block with an empty environment. """ def with_agent(%Connection{ssh_key: key}, host_key, fun) when is_binary(key) do dir = private_dir()
try do socket = Path.join(dir, "agent") known_hosts = Path.join(dir, "known_hosts")
write(known_hosts, host_key)
case start_agent(socket) do {:ok, pid} -> try do case add_key(socket, key) do :ok -> fun.(env(socket, known_hosts)) {:error, output} -> {:error, output} end after stop_agent(pid) end
{:error, output} -> {:error, output} end |> scrub(dir) after File.rm_rf!(dir) end end
def with_agent(%Connection{}, _host_key, fun), do: fun.([])
@doc """ Generates the keypair git-sync pushes with. The operator never sees the private half; the public half is theirs to add to the knot. """ def generate_key do dir = private_dir() path = Path.join(dir, "id_ed25519")
try do case System.cmd("ssh-keygen", ~w(-q -t ed25519 -N) ++ ["", "-C", "git-sync", "-f", path], stderr_to_stdout: true ) do {_output, 0} -> {:ok, %{private: File.read!(path), public: File.read!(path <> ".pub")}} {output, _} -> {:error, scrub_dir(output, dir)} end after File.rm_rf!(dir) end end
@doc """ Asks a host which keys it identifies itself by, in `known_hosts` form.
This is trust on first use: whatever answers on the wire is what gets believed. Show `fingerprints/1` of the result to whoever is setting the connection up so they can check it against what the host's operator publishes. """ def scan_host(host, port \\ default_port()) do {output, _status} = System.cmd("ssh-keyscan", ["-T", @timeout, "-p", "#{port}", host], stderr_to_stdout: true)
case keys(output) do [] -> {:error, "no host keys came back from #{host}"} keys -> {:ok, Enum.join(keys, "\n") <> "\n"} end end
@doc """ The fingerprints of every key in a `known_hosts` blob, for an operator to compare against the host they meant to reach. Anything unreadable fingerprints as nothing. """ def fingerprints(blob) when blob in [nil, ""], do: []
def fingerprints(blob) do dir = private_dir() path = Path.join(dir, "known_hosts")
try do write(path, blob)
case System.cmd("ssh-keygen", ["-lf", path], stderr_to_stdout: true) do {output, 0} -> Enum.map(lines(output), &Enum.at(String.split(&1, " "), 1)) {_output, _} -> [] end after File.rm_rf!(dir) end end
defp default_port, do: Application.get_env(:git_sync, :knot_ssh_port, 22)
defp keys(output), do: Enum.reject(lines(output), &String.starts_with?(&1, "#"))
defp lines(output), do: output |> String.split("\n", trim: true) |> Enum.map(&String.trim/1)
defp env(socket, known_hosts) do command = Enum.join(["ssh" | @options] ++ ["-o UserKnownHostsFile=#{known_hosts}"], " ")
[{"SSH_AUTH_SOCK", socket}, {"GIT_SSH_COMMAND", command}] end
defp start_agent(socket) do case System.cmd("ssh-agent", ["-a", socket], stderr_to_stdout: true) do {output, 0} -> {:ok, agent_pid(output)} {output, _} -> {:error, output} end end
defp agent_pid(output) do [pid] = Regex.run(~r/SSH_AGENT_PID=(\d+)/, output, capture: :all_but_first) pid end
defp stop_agent(pid) do System.cmd("ssh-agent", ["-k"], env: [{"SSH_AGENT_PID", pid}], stderr_to_stdout: true) end
defp add_key(socket, key) do env = [{"SSH_AUTH_SOCK", socket}, {"GIT_SYNC_SSH_KEY", key}]
case System.cmd("sh", ["-c", @add], env: env, stderr_to_stdout: true) do {_output, 0} -> :ok {output, _} -> {:error, output} end end
defp private_dir do path = Path.join(System.tmp_dir!(), "git-sync-ssh-#{System.unique_integer([:positive])}")
File.mkdir_p!(path) File.chmod!(path, 0o700) path end
defp write(path, contents) do File.write!(path, String.trim_trailing("#{contents}") <> "\n") File.chmod!(path, 0o600) end
defp scrub({status, output}, dir), do: {status, scrub_dir(output, dir)}
defp scrub_dir(output, dir), do: String.replace(output, dir, "<ssh>")end