3.6 KiB · text History 6bfb936
defmodule GitGud.Packages.Apt.Native do
  @moduledoc """
  The two things the apt repository needs that OTP cannot do, via the
  `native/gitgud_apt` Rustler NIF.

  **OpenPGP signatures.** apt requires a signed `Release`, and
  `:public_key` speaks PKCS and X.509 — OpenPGP is a different packet
  format with its own key material, and nothing in the Erlang
  distribution implements it. This is the only format in the registry
  that needed a NIF; the rest get by on `:crypto` and `:public_key`.

  **xz and zstd.** A `.deb`'s control member is `control.tar.xz` on
  anything since squeeze and `control.tar.zst` on Ubuntu, and
  `:erl_tar` handles gzip and nothing else. The same encoder produces
  the `Packages.xz` a Debian repository is expected to offer.

  ## Why these backends

  `sequoia-openpgp` defaults to Nettle, which would put `libnettle`
  and `libgmp` in the runtime image. This project ships k8s manifests;
  a NIF that changes the image's package list is a deployment change
  wearing a dependency's clothes. The pure-Rust backend needs no
  system libraries at all, but sequoia gates it behind
  `allow-variable-time-crypto`, and this code holds a private key. The
  OpenSSL backend links libssl and libcrypto, which are already in
  both images — the builder installs `libssl-dev` for gix and the
  runner installs `openssl` for the Erlang runtime — and is
  constant-time. So: OpenSSL, and no image change at all.

  The compression crates (`lzma-rs`, `ruzstd`) are pure Rust for the
  same reason: no new system libraries.

  Keys and signatures cross the boundary as armored ASCII, so nothing
  on the Elixir side handles OpenPGP packets.
  """

  use Rustler, otp_app: :git_gud, crate: "gitgud_apt"

  @doc """
  Generate a signing certificate for `user_id`. Returns
  `{:ok, {public_armored, secret_armored}}` — the public half goes in
  a client's `/etc/apt/keyrings`, the secret half is stored encrypted
  and never leaves the forge.
  """
  @spec generate_key(String.t()) :: {:ok, {String.t(), String.t()}} | {:error, String.t()}
  def generate_key(_user_id), do: :erlang.nif_error(:nif_not_loaded)

  @doc """
  Clearsign `data` — the `InRelease` shape, signature and signed text
  in one file, which is what apt prefers.
  """
  @spec sign_clearsign(String.t(), String.t()) :: {:ok, String.t()} | {:error, String.t()}
  def sign_clearsign(_secret_armored, _data), do: :erlang.nif_error(:nif_not_loaded)

  @doc """
  Produce a detached armored signature — the `Release.gpg` shape,
  which older apt falls back to when `InRelease` is absent.
  """
  @spec sign_detached(String.t(), String.t()) :: {:ok, String.t()} | {:error, String.t()}
  def sign_detached(_secret_armored, _data), do: :erlang.nif_error(:nif_not_loaded)

  @doc """
  Verify a detached signature. Not used to serve anything — it lets
  tests assert what apt will assert.
  """
  @spec verify_detached(String.t(), String.t(), String.t()) ::
          {:ok, boolean()} | {:error, String.t()}
  def verify_detached(_public_armored, _data, _signature_armored),
    do: :erlang.nif_error(:nif_not_loaded)

  @doc "Decompress an xz stream."
  @spec xz_decompress(binary()) :: {:ok, binary()} | {:error, String.t()}
  def xz_decompress(_data), do: :erlang.nif_error(:nif_not_loaded)

  @doc "Compress to xz."
  @spec xz_compress(binary()) :: {:ok, binary()} | {:error, String.t()}
  def xz_compress(_data), do: :erlang.nif_error(:nif_not_loaded)

  @doc "Decompress a zstd stream."
  @spec zstd_decompress(binary()) :: {:ok, binary()} | {:error, String.t()}
  def zstd_decompress(_data), do: :erlang.nif_error(:nif_not_loaded)
end