#!/bin/bash
#
# Integrity verification of the installed LibrePortal code tree.
#
# Re-hashes $script_dir against the signed SHA256SUMS manifest that ships INSIDE
# every release tarball (built by scripts/release/make_release.sh) and checks
# that manifest's minisign signature against the ROOT-OWNED public key in the
# footprint (/usr/local/lib/libreportal/libreportal.pub). A pass proves the
# installed files still match the release we published and signed.
#
# Honest scope: this runs BY the software it verifies, so it catches accidental
# drift, partial tampering, and "is this a real signed release" — but it is NOT
# tamper-proof on its own (a wholesale code swap could also fake the result).
# The hard guarantee is out-of-band: the user running `minisign -Vm <tarball>`.
#
# Results land in LP_VERIFY_* globals so the generator + CLI can format them:
#   LP_VERIFY_STATE      verified|modified|tampered|unsigned|unverifiable|development
#   LP_VERIFY_SIGNED     true|false              (a real, non-placeholder key signed it)
#   LP_VERIFY_SIG_VALID  true|false|null         (null = not applicable / couldn't check)
#   LP_VERIFY_TOTAL      files listed in the manifest
#   LP_VERIFY_OK / _MODIFIED / _MISSING
#   LP_VERIFY_SAMPLE     up to 5 offending relative paths, newline-separated
#   LP_VERIFY_ERROR      human message, or empty

lpVerifyPubKeyPath() { echo "/usr/local/lib/libreportal/libreportal.pub"; }

lpVerifyInstall() {
    LP_VERIFY_STATE="development"
    LP_VERIFY_SIGNED="false"
    LP_VERIFY_SIG_VALID="null"
    LP_VERIFY_TOTAL=0; LP_VERIFY_OK=0; LP_VERIFY_MODIFIED=0; LP_VERIFY_MISSING=0
    LP_VERIFY_SAMPLE=""
    LP_VERIFY_ERROR=""

    local mode="${CFG_INSTALL_MODE:-release}"
    local root="${script_dir%/}"
    local manifest="$root/SHA256SUMS"
    local sig="$root/SHA256SUMS.minisig"
    local pub; pub="$(lpVerifyPubKeyPath)"

    # No manifest — a git/local dev install, or a build without one. There's
    # nothing to verify against, so report a neutral "development build" rather
    # than implying something is wrong.
    if [[ "$mode" != "release" || ! -f "$manifest" ]]; then
        LP_VERIFY_STATE="development"
        return 0
    fi

    LP_VERIFY_TOTAL=$(wc -l < "$manifest" 2>/dev/null | tr -d ' ')
    [[ -n "$LP_VERIFY_TOTAL" ]] || LP_VERIFY_TOTAL=0

    # Manifest signature. The pubkey is in the root-owned footprint so the
    # manager can't swap it to bless a forged manifest. A REPLACE_ME placeholder
    # means signing isn't activated for this build → treat the manifest as
    # unsigned (drift detection still works, we just can't vouch for it).
    if [[ -f "$pub" ]] && ! grep -q REPLACE_ME "$pub" 2>/dev/null; then
        LP_VERIFY_SIGNED="true"
        if ! command -v minisign >/dev/null 2>&1; then
            LP_VERIFY_STATE="unverifiable"
            LP_VERIFY_ERROR="minisign is not installed, so the signed manifest can't be checked."
            return 0
        fi
        if [[ ! -f "$sig" ]]; then
            LP_VERIFY_STATE="tampered"
            LP_VERIFY_SIG_VALID="false"
            LP_VERIFY_ERROR="The release manifest signature (SHA256SUMS.minisig) is missing."
            return 0
        fi
        if ! minisign -Vm "$manifest" -p "$pub" -x "$sig" >/dev/null 2>&1; then
            LP_VERIFY_STATE="tampered"
            LP_VERIFY_SIG_VALID="false"
            LP_VERIFY_ERROR="The release manifest signature is invalid."
            return 0
        fi
        LP_VERIFY_SIG_VALID="true"
    fi

    # Re-hash every listed file. `--check --quiet` prints only failures; each is
    # either "<path>: FAILED" (content differs) or "<path>: FAILED open or read"
    # (missing/unreadable). Lines prefixed "sha256sum:" are diagnostics/summary
    # — skip them so a missing file isn't double-counted. Run from $root so the
    # manifest's ./relative paths resolve.
    local check_out line path sample_count=0
    check_out="$(cd "$root" && sha256sum --check --quiet "$manifest" 2>&1)"
    while IFS= read -r line; do
        [[ -z "$line" || "$line" == sha256sum:* ]] && continue
        if [[ "$line" == *": FAILED open or read" ]]; then
            LP_VERIFY_MISSING=$((LP_VERIFY_MISSING + 1)); path="${line%: FAILED open or read}"
        elif [[ "$line" == *": FAILED" ]]; then
            LP_VERIFY_MODIFIED=$((LP_VERIFY_MODIFIED + 1)); path="${line%: FAILED}"
        else
            continue
        fi
        if (( sample_count < 5 )); then
            LP_VERIFY_SAMPLE+="${LP_VERIFY_SAMPLE:+$'\n'}${path#./}"
            sample_count=$((sample_count + 1))
        fi
    done <<< "$check_out"

    local bad=$((LP_VERIFY_MODIFIED + LP_VERIFY_MISSING))
    LP_VERIFY_OK=$((LP_VERIFY_TOTAL - bad))
    (( LP_VERIFY_OK < 0 )) && LP_VERIFY_OK=0

    if (( bad > 0 )); then
        LP_VERIFY_STATE="modified"
    elif [[ "$LP_VERIFY_SIGNED" == "true" ]]; then
        LP_VERIFY_STATE="verified"
    else
        LP_VERIFY_STATE="unsigned"
    fi
    return 0
}
