2.3 KiB · text History 3f25757
defmodule GitGud.Packages.Signing.Local do
  @moduledoc """
  The default signing backend: the private key lives in Postgres,
  AES-GCM-encrypted through `GitGud.Secrets`, and is decrypted in the
  app to sign.

  This is what every registry format did before the behaviour existed,
  and it stays the default — an existing deployment changes nothing and
  needs nothing new running.

  It delegates key custody to `GitGud.Packages.signing_key/2`, so apt's
  OpenPGP certificate and the RSA keys every other format uses are
  reached the same way they always were. Only `sign/4` is new, and it
  only knows RSA: apt does its own signing through the sequoia NIF
  because OpenPGP is a packet format, not a raw signature.
  """
  @behaviour GitGud.Packages.Signing

  alias GitGud.Packages

  @impl true
  def ensure_key(scope, format) do
    # Only the public half leaves: `signing_key/2` also hands back the
    # private key.
    with {:ok, key} <- Packages.signing_key(scope, format) do
      {:ok, Map.take(key, [:public_pem])}
    end
  end

  @impl true
  def public_key(scope, format) do
    case Packages.get_signing_key(scope, format) do
      nil -> {:error, :no_key}
      key -> {:ok, key.public_key}
    end
  end

  @impl true
  def sign(scope, format, data, opts) when is_binary(data) do
    digest = Keyword.get(opts, :digest, :sha256)
    scheme = Keyword.get(opts, :scheme, :pkcs1v15)

    with {:ok, %{private_pem: pem}} <- Packages.signing_key(scope, format),
         {:ok, key} <- decode_private(pem) do
      erlang_digest = GitGud.Packages.Signing.erlang_digest(digest)
      {:ok, :public_key.sign(data, erlang_digest, key, sign_options(scheme))}
    end
  rescue
    # `:public_key.sign/4` raises on a key it can't use — a caller
    # asking an OpenPGP certificate to produce a PKCS#1 signature, for
    # instance. An error tuple is the useful shape here.
    e -> {:error, {:sign_failed, Exception.message(e)}}
  end

  defp sign_options(:pss),
    do: [rsa_padding: :rsa_pkcs1_pss_padding, rsa_pss_saltlen: -1]

  defp sign_options(_pkcs1v15), do: []

  defp decode_private(pem) do
    case :public_key.pem_decode(pem) do
      [entry | _] -> {:ok, :public_key.pem_entry_decode(entry)}
      [] -> {:error, :not_a_pem_key}
    end
  rescue
    _ -> {:error, :not_a_pem_key}
  end
end