Wren Linux

wren-linux / lib / apt-keys.sh

# shellcheck shell=bash
# lib/apt-keys.sh: the signing keys of the apt repositories Wren Linux adds,
# kept fresh. Sourced after lib.sh, by update.sh and status.sh.
#
# A key is installed when its extra runs, and an extra only runs again when
# its own files change. A key that expires (Signal's, Mullvad's, or the Tor
# Project's, whose signing subkey expires 2027-11-01) or that the vendor
# renews would then sit there unchanged: apt stops taking that repository's
# updates, and unattended-upgrades does so quietly. So update.sh checks each
# installed key every time, and fetches a new one when it can no longer sign
# or will stop within APT_KEY_WARN_DAYS days; the new key goes in only if it
# is exactly the pinned one (key_ok) and signs for longer than the old.
# status.sh shows the same check.

# NAME|FILE|FINGERPRINT VARIABLE|URL VARIABLE (pins.sh); FILE is where the
# script that adds the repository puts its key (a test checks they agree)
APT_KEYS=(
    "Claude Code|/etc/apt/keyrings/claude-code.asc|ANTHROPIC_FPR|ANTHROPIC_KEY_URL"
    "Signal|/usr/share/keyrings/signal-desktop-keyring.asc|SIGNAL_FPR|SIGNAL_KEY_URL"
    "Mullvad|/usr/share/keyrings/mullvad-keyring.asc|MULLVAD_FPR|MULLVAD_KEY_URL"
    "the Tor Project (lyrebird)|/usr/share/keyrings/wren-torproject.asc|TORPROJECT_FPR|TORPROJECT_KEY_URL"
)
APT_KEY_WARN_DAYS=60
# (a test reads the key files from under this folder; sudo still writes to the real path)
APT_KEYS_ROOT=${WREN_KEYS_ROOT:-}

# key_sign_until FILE: until when (seconds since 1970; 0 for never) a key in
# FILE that isn't expired or revoked can sign; fails if none can
key_sign_until() {
    gpg --show-keys --with-colons "$1" 2>/dev/null | awk -F: '
        /^(pub|sub):/ && $2 !~ /[erd]/ && $12 ~ /s/ {
            found = 1
            if ($7 == "") never = 1; else if ($7 + 0 > max) max = $7 + 0
        }
        END { if (!found) exit 1; print (never ? 0 : max) }'
}

# apt_key_state FILE FPR: "ok", "soon DAYS" (it stops signing in DAYS days),
# or "bad" (not the pinned key, or it can't sign any more)
apt_key_state() {
    local until days
    key_ok "$1" "$2" && until=$(key_sign_until "$1") || { echo bad; return; }
    [ "$until" != 0 ] || { echo ok; return; }
    days=$(((until - $(date +%s)) / 86400))
    if [ "$days" -lt "$APT_KEY_WARN_DAYS" ]; then echo "soon $days"; else echo ok; fi
}

# apt_keys_installed: the APT_KEYS lines whose key file is on this computer
apt_keys_installed() {
    local k file
    for k in "${APT_KEYS[@]}"; do
        IFS='|' read -r _ file _ _ <<<"$k"
        [ ! -e "$APT_KEYS_ROOT$file" ] || echo "$k"
    done
}

# apt_keys_status: one line per installed key, for status.sh
apt_keys_status() {
    local name file fv uv state
    while IFS='|' read -r name file fv uv; do
        state=$(apt_key_state "$APT_KEYS_ROOT$file" "${!fv}")
        case $state in
            ok) printf '  [x] %s\n' "$name" ;;
            soon*) printf '  [!] %s: stops signing in %s days. bash update.sh fetches the new key\n' "$name" "${state#soon }" ;;
            *) printf '  [ ] %s: EXPIRED or not the pinned key, so apt takes no updates from it. bash update.sh fetches the new key\n' "$name" ;;
        esac
    done < <(apt_keys_installed)
}

# apt_keys_refresh: a new key for each installed one that can't sign, or
# stops soon. Never stops update.sh; returns 1 if a key is still bad after.
apt_keys_refresh() {
    local name file fv uv state tmp old new rc=0
    while IFS='|' read -r name file fv uv; do
        state=$(apt_key_state "$APT_KEYS_ROOT$file" "${!fv}")
        [ "$state" != ok ] || continue
        note "The signing key for $name $([ "$state" = bad ] && echo "can't sign any more" || echo "stops signing in ${state#soon } days"): fetching it again"
        tmp=$(mktemp)
        if ! wget -qO "$tmp" "${!uv}"; then
            warn "Couldn't download it from ${!uv}; kept the old one."
        elif ! key_ok "$tmp" "${!fv}"; then
            warn "The key at ${!uv} isn't exactly the pinned key ${!fv} (or it has expired). Not trusting it; kept the old one."
        else
            new=$(key_sign_until "$tmp") old=$(key_sign_until "$APT_KEYS_ROOT$file" || echo -1)
            # 0 is never: longer than any date
            if [ "$new" = 0 ] || { [ "$old" != 0 ] && [ "$new" -gt "$old" ]; }; then
                sudo install -m 0644 "$tmp" "$file"
                note "Installed the renewed key for $name."
                state=$(apt_key_state "$APT_KEYS_ROOT$file" "${!fv}")
            else
                warn "$name hasn't renewed its key yet; update.sh will look again next time."
            fi
        fi
        rm -f "$tmp"
        [ "$state" != bad ] || {
            warn "apt can't check $name's updates until this key is renewed."
            rc=1
        }
    done < <(apt_keys_installed)
    return "$rc"
}