4.0 KiB · text History 9049eaf
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

  This project ships k8s manifests, so a NIF that changes an image's
  package list is a deployment change wearing a dependency's clothes.
  The cost was measured by building the Dockerfile, not reasoned about:

      crypto-openssl   builder: libclang-dev      runtime: — (libssl already named)
      crypto-nettle    builder: libclang-dev,     runtime: — (but libnettle/libgmp
                                nettle-dev,                 are present only as a
                                libgmp-dev                  transitive dep of git)
      crypto-rust      builder: —                 runtime: —   variable-time

  `libclang-dev` is unavoidable for any backend that binds C:
  `openssl-sys` generates its bindings with bindgen, which dlopens
  libclang. Only the pure-Rust backend escapes it, and sequoia gates
  that behind `allow-variable-time-crypto` — the wrong trade for code
  holding a signing key.

  So: OpenSSL. One package, in the build stage only, for constant-time
  audited crypto. The runtime image is unchanged.

  The compression crates (`lzma-rs`, `ruzstd`) are pure Rust and cost
  nothing either way.

  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