#!/bin/bash
# Add the StackX Bullseye amd64 kernel archive and refresh only that source.
# Does not install a package and does not reboot.
#
# Minimum dependencies, all already required by a normal Bullseye apt host
# except gnupg, which is required here for a real fingerprint check:
#   bash, coreutils, diffutils (cmp), grep, dpkg, apt (apt-get and
#   /usr/lib/apt/apt-helper), ca-certificates, gnupg (gpg).
# Bullseye apt speaks HTTPS itself. apt-transport-https, python, wget, and
# curl are not used.
#
# Debian 11 normally links /etc/os-release to ../usr/lib/os-release. A regular
# file is accepted. Any other symlink target is refused.
#
# EXPECTED_FPR pins the dedicated primary repository signing key.
# Any rotation requires an explicitly reviewed update of this fingerprint.

set -euo pipefail
umask 022

EXPECTED_FPR='AF97ED98AACE4CE129A03B6E2C9736B593BABF7F'

ORIGIN='https://mirrors.scalarx.net/kernel/bullseye'
KEY_URL="${ORIGIN}/stackx-kernel-bullseye.asc"
KEYRING='/usr/share/keyrings/stackx-kernel-bullseye.gpg'
LIST='/etc/apt/sources.list.d/stackx-kernel-bullseye.list'
HELPER='/usr/lib/apt/apt-helper'

die() {
  echo "error: $*" >&2
  exit 1
}

if [[ $# -ne 0 ]]; then
  echo "usage: add-stackx-kernel-repo.sh" >&2
  echo "Adds the repository and refreshes only its index. It does not install or reboot." >&2
  exit 1
fi

if [[ "$(id -u)" -ne 0 ]]; then
  die "root is required"
fi

# Accept a regular /etc/os-release, or a symlink that resolves to exactly the
# regular file /usr/lib/os-release (Debian's ../usr/lib/os-release). Read that
# regular file afterwards. Do not follow any other symlink target.
os_release_path=
if [[ -L /etc/os-release ]]; then
  if ! command -v readlink >/dev/null 2>&1; then
    die "missing required command: readlink"
  fi
  os_canon=$(readlink -f -- /etc/os-release) || die "could not resolve /etc/os-release"
  if [[ "$os_canon" != /usr/lib/os-release ]]; then
    die "refusing /etc/os-release symlink target (only /usr/lib/os-release is accepted)"
  fi
  if [[ -L /usr/lib/os-release || ! -f /usr/lib/os-release ]]; then
    die "refusing /etc/os-release: resolved /usr/lib/os-release is not a regular file"
  fi
  os_release_path=/usr/lib/os-release
elif [[ -f /etc/os-release ]]; then
  os_release_path=/etc/os-release
else
  die "need /etc/os-release as a regular file or a symlink to the regular file /usr/lib/os-release"
fi

os_id=
os_codename=
while IFS= read -r line || [[ -n "$line" ]]; do
  case "$line" in
    ID=*|VERSION_CODENAME=*)
      key=${line%%=*}
      val=${line#*=}
      val=${val#\"}
      val=${val%\"}
      case "$key" in
        ID) os_id=$val ;;
        VERSION_CODENAME) os_codename=$val ;;
      esac
      ;;
  esac
done < "$os_release_path"

if ! command -v dpkg >/dev/null 2>&1; then
  die "dpkg is required to confirm the architecture"
fi
arch=$(dpkg --print-architecture)

if [[ "$os_codename" == stretch || "$os_codename" == buster ]]; then
  die "Debian ${os_codename} is not accepted. This installer does not write a source entry for it"
fi
if [[ "$os_id" != debian || "$os_codename" != bullseye || "$arch" != amd64 ]]; then
  die "only Debian bullseye amd64 is accepted (found ID=${os_id:-unknown} VERSION_CODENAME=${os_codename:-unknown} arch=${arch})"
fi

fingerprint_is_final() {
  local f u
  f=$1
  [[ "$f" =~ ^[0-9A-Fa-f]{40}$ ]] || return 1
  u=$(printf '%s' "$f" | tr '[:lower:]' '[:upper:]')
  [[ "$u" != 0000000000000000000000000000000000000000 ]] || return 1
  [[ "$u" != FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF ]] || return 1
  return 0
}

if ! fingerprint_is_final "$EXPECTED_FPR"; then
  die "EXPECTED_FPR is not a finalized 40-hex primary fingerprint. Replace the placeholder before use. No key was downloaded and no apt file was written"
fi

for cmd in gpg cmp mktemp chmod ln cp cat tr wc grep mkdir rm id readlink; do
  if ! command -v "$cmd" >/dev/null 2>&1; then
    die "missing required command: ${cmd}"
  fi
done
if [[ ! -x "$HELPER" ]]; then
  die "missing ${HELPER} from package apt"
fi
if ! command -v apt-get >/dev/null 2>&1; then
  die "apt-get is required"
fi
if [[ ! -s /etc/ssl/certs/ca-certificates.crt || -L /etc/ssl/certs/ca-certificates.crt ]]; then
  die "ca-certificates bundle /etc/ssl/certs/ca-certificates.crt is missing"
fi

case "$KEY_URL" in
  https://mirrors.scalarx.net/kernel/bullseye/*) ;;
  *) die "internal error: key URL is not the Bullseye HTTPS mirror" ;;
esac
case "$ORIGIN" in
  https://mirrors.scalarx.net/kernel/bullseye) ;;
  *) die "internal error: archive origin is not the Bullseye HTTPS mirror" ;;
esac

source_line="deb [arch=amd64 signed-by=${KEYRING}] ${ORIGIN} ./"
case "$source_line" in
  *trusted=yes*|*allow-unauthenticated*|*AllowInsecure*|*AllowUnauthenticated*)
    die "internal error: source line would weaken signature checks"
    ;;
esac

work=$(mktemp -d /tmp/stackx-kernel-repo.XXXXXX)
chmod 700 "$work"
trap 'rm -rf "$work"' EXIT

unset APT_CONFIG
if ! "$HELPER" \
  -o Acquire::https::Verify-Peer=true \
  -o Acquire::https::Verify-Host=true \
  download-file "$KEY_URL" "$work/key.asc" \
  >"$work/download.log" 2>&1; then
  echo "error: download of the repository public key failed" >&2
  cat "$work/download.log" >&2
  exit 1
fi

key_bytes=$(wc -c < "$work/key.asc")
if (( key_bytes < 32 || key_bytes > 65536 )); then
  die "refusing key file of unexpected size (${key_bytes} bytes)"
fi
if ! grep -q -- '-----BEGIN PGP PUBLIC KEY BLOCK-----' "$work/key.asc"; then
  die "repository key is not an armored public key"
fi
if grep -q -- 'PRIVATE KEY' "$work/key.asc"; then
  die "refusing secret key material"
fi

export GNUPGHOME="$work/gnupg"
mkdir -m 700 "$GNUPGHOME"

if ! gpg --batch --no-options --no-autostart --with-colons --show-keys \
  "$work/key.asc" >"$work/show.txt" 2>"$work/gpg.err"; then
  echo "error: gpg could not read the downloaded key" >&2
  cat "$work/gpg.err" >&2
  exit 1
fi

# Field 2 is validity. Field 10 is the fingerprint on fpr records.
# The primary fingerprint is the fpr record that follows pub, before any sub.
parse_primary() {
  local rectype field2 field10
  local pubs=0
  local state=none
  local fpr=
  local validity=
  while IFS=: read -r rectype field2 _ _ _ _ _ _ _ field10 _; do
    case "$rectype" in
      pub)
        pubs=$((pubs + 1))
        if (( pubs > 1 )); then
          echo "key file contains more than one primary key" >&2
          return 1
        fi
        validity=$field2
        state=need_fpr
        ;;
      fpr)
        if [[ "$state" == need_fpr ]]; then
          fpr=$field10
          state=got_fpr
        fi
        ;;
      sub)
        if [[ "$state" == need_fpr ]]; then
          echo "primary key has no fingerprint record" >&2
          return 1
        fi
        state=sub
        ;;
      sec|ssb)
        echo "refusing secret key material" >&2
        return 1
        ;;
      rvk)
        echo "refusing revoked key" >&2
        return 1
        ;;
    esac
  done < "$1"
  if (( pubs != 1 )); then
    echo "key file does not contain exactly one primary key" >&2
    return 1
  fi
  case "$validity" in
    r|e|i|d|n)
      echo "refusing primary key with gpg validity '${validity}'" >&2
      return 1
      ;;
  esac
  if [[ ! "$fpr" =~ ^[0-9A-Fa-f]{40}$ ]]; then
    echo "primary fingerprint missing or not 40 hex digits" >&2
    return 1
  fi
  printf '%s\n' "$fpr"
}

if ! got_fpr=$(parse_primary "$work/show.txt"); then
  die "could not verify the primary fingerprint"
fi
exp_u=$(printf '%s' "$EXPECTED_FPR" | tr '[:lower:]' '[:upper:]')
got_u=$(printf '%s' "$got_fpr" | tr '[:lower:]' '[:upper:]')
if [[ "$got_u" != "$exp_u" ]]; then
  echo "error: primary fingerprint mismatch" >&2
  echo "observed ${got_u}" >&2
  echo "expected ${exp_u}" >&2
  echo "no apt file was written" >&2
  exit 1
fi

if ! gpg --batch --no-options --no-autostart --dearmor \
  --output "$work/keyring.gpg" "$work/key.asc" 2>"$work/dearmor.err"; then
  echo "error: could not dearmor the public key" >&2
  cat "$work/dearmor.err" >&2
  exit 1
fi
ring_bytes=$(wc -c < "$work/keyring.gpg")
if (( ring_bytes < 32 || ring_bytes > 65536 )); then
  die "refusing dearmored key of unexpected size (${ring_bytes} bytes)"
fi
if ! gpg --batch --no-options --no-autostart --with-colons --show-keys \
  "$work/keyring.gpg" >"$work/show-ring.txt" 2>"$work/gpg-ring.err"; then
  echo "error: gpg could not read the dearmored keyring" >&2
  cat "$work/gpg-ring.err" >&2
  exit 1
fi
if ! ring_fpr=$(parse_primary "$work/show-ring.txt"); then
  die "dearmored keyring failed the primary-key check"
fi
ring_u=$(printf '%s' "$ring_fpr" | tr '[:lower:]' '[:upper:]')
if [[ "$ring_u" != "$got_u" ]]; then
  die "dearmored keyring fingerprint does not match the armored key"
fi

{
  echo "# Managed by add-stackx-kernel-repo.sh. StackX Bullseye amd64 kernel archive."
  printf '%s\n' "$source_line"
} > "$work/source.list"
deb_lines=$(grep -c '^deb ' "$work/source.list" || true)
if [[ "$deb_lines" -ne 1 ]]; then
  die "internal error: expected exactly one deb line"
fi
if grep -Eq 'trusted=yes|allow-unauthenticated|AllowInsecure|AllowUnauthenticated' "$work/source.list"; then
  die "internal error: generated source list weakens signature checks"
fi

# Install a regular file without following a symlink and without replacing
# unrelated content. Identical content is success. Anything else is a refusal.
install_exact() {
  local dest=$1 src=$2
  local dir tmp base
  dir=$(dirname -- "$dest")
  base=$(basename -- "$dest")
  if [[ -L "$dir" || ! -d "$dir" ]]; then
    echo "refusing ${dir}: not a real directory" >&2
    return 1
  fi
  if [[ -L "$dest" ]]; then
    echo "refusing symlink ${dest}" >&2
    return 1
  fi
  if [[ -e "$dest" && ! -f "$dest" ]]; then
    echo "refusing non-regular file ${dest}" >&2
    return 1
  fi
  if [[ -f "$dest" ]]; then
    if cmp -s "$src" "$dest"; then
      chmod 644 "$dest"
      return 0
    fi
    echo "unexpected existing content in ${dest}; not replacing it" >&2
    return 1
  fi
  tmp=$(mktemp "${dir}/.${base}.XXXXXX")
  cp -- "$src" "$tmp"
  chmod 644 "$tmp"
  if ln -- "$tmp" "$dest"; then
    rm -f -- "$tmp"
    return 0
  fi
  rm -f -- "$tmp"
  if [[ -L "$dest" ]]; then
    echo "refusing symlink ${dest}" >&2
    return 1
  fi
  if [[ -f "$dest" ]] && cmp -s "$src" "$dest"; then
    chmod 644 "$dest"
    return 0
  fi
  echo "could not create ${dest}" >&2
  return 1
}

if [[ -L /usr/share || ! -d /usr/share ]]; then
  die "refusing /usr/share: not a real directory"
fi
if [[ -L /usr/share/keyrings ]]; then
  die "refusing symlink /usr/share/keyrings"
fi
if [[ ! -d /usr/share/keyrings ]]; then
  if [[ -e /usr/share/keyrings ]]; then
    die "unexpected /usr/share/keyrings"
  fi
  mkdir -m 755 /usr/share/keyrings
fi
if [[ -L /etc/apt || ! -d /etc/apt ]]; then
  die "refusing /etc/apt: not a real directory"
fi
if [[ -L /etc/apt/sources.list.d || ! -d /etc/apt/sources.list.d ]]; then
  die "need a real directory /etc/apt/sources.list.d"
fi

install_exact "$KEYRING" "$work/keyring.gpg"
install_exact "$LIST" "$work/source.list"

# Dir::Etc::sourceparts=- reads no other files under sources.list.d.
# List-Cleanup=0 does not delete indexes that belong to other sources.
# APT::Update::Error-Mode=any (apt 2.2) makes any index error a failed update.
# Without it, apt-get update can exit 0 after a partial refresh.
# The false flags override a local apt.conf that would accept unsigned indexes.
# trusted=yes is not used. TLS and signature checks stay strict.
if ! apt-get update \
  -o Dir::Etc::sourcelist="sources.list.d/stackx-kernel-bullseye.list" \
  -o Dir::Etc::sourceparts="-" \
  -o APT::Get::List-Cleanup="0" \
  -o APT::Update::Error-Mode=any \
  -o Acquire::AllowInsecureRepositories=false \
  -o Acquire::AllowDowngradeToInsecureRepositories=false \
  -o APT::Get::AllowUnauthenticated=false \
  -o Acquire::https::Verify-Peer=true \
  -o Acquire::https::Verify-Host=true; then
  echo "error: index refresh failed for the StackX source only" >&2
  echo "signature checks were not disabled. No package was installed and the system was not rebooted." >&2
  exit 1
fi

# Bilan et étapes suivantes communs aux trois distributions.
printf '%s\n' \
  'Dépôt noyau StackX Bullseye configuré.' \
  'Clé du dépôt et index APT vérifiés.' \
  'Aucun paquet installé, aucun redémarrage, aucun choix de démarrage modifié.' \
  '' \
  '1. Simuler l'\''installation et examiner les opérations proposées :' \
  '   apt-get --simulate --no-remove install stackx-kernel-bullseye-amd64' \
  '2. Si la simulation convient, installer explicitement :' \
  '   apt-get --no-remove install stackx-kernel-bullseye-amd64' \
  '' \
  'Ces commandes sont affichées uniquement ; le script ne les exécute pas.' \
  'Avant de redémarrer, vérifier le noyau sélectionné dans GRUB (voir README.txt).' \
  'Après redémarrage : uname -r doit afficher 5.10.0-46-sx1-amd64.'
