File fix-apt-update-error-expired-key-missing-release-file of Package kimi-utils-ubuntu
#!/usr/bin/env bash
# fix-apt-update-error-expired-key-missing-release-file.sh
# Detects apt-update errors (404, missing Release, EXPKEYSIG, …)
# and renames the offending source files and matching expired GPG keys.
#
# This is a cleaned-up, functionally equivalent refactor of the working script.
# Behaviour, messages and exit codes are preserved.
set -eo pipefail
# -------------------------
# Configuration (read-only)
# -------------------------
readonly SRC_DIR="/etc/apt/sources.list.d"
readonly APT_TRUST_DIR="/etc/apt/trusted.gpg.d"
readonly PATTERNS=("*.list" "*.sources")
readonly ERR_REGEX='Err:|EXPKEYSIG|GPG error|Failed to fetch|404|Not Found|does not have a Release file|is not signed'
export LANG=C
# -------------------------
# Runtime state / counters
# -------------------------
declare -A url_to_sig # URL -> signature (empty if none)
declare -a all_src_files # collected source files
declare -a expired_keys # will hold matching GPG key paths (list form)
declare -A expired_keys_fps # map keyfile -> matching fprs csv
current_url=""
renamed_sources=0
renamed_keys=0
# -------------------------
# Logging helpers
# -------------------------
log() { printf '[*] %s\n' "$*"; }
warn() { printf '[!] %s\n' "$*" >&2; }
err() { printf '[ERROR] %s\n' "$*" >&2; }
# -------------------------
# Utility helpers
# -------------------------
timestamp() { date +"%Y-%m-%d_%H-%M-%S"; }
# Append value to an array if not already present
unique_append() {
local -n arr=$1; local val=$2
for e in "${arr[@]:-}"; do [[ "$e" == "$val" ]] && return; done
arr+=("$val")
}
# Simple validation for http(s) URL
is_http_url() { [[ $1 =~ ^https?:// ]]; }
# Extract the hex signature from an EXPKEYSIG line (returns empty if none)
extract_sig() { grep -oP 'EXPKEYSIG \K[0-9A-F]+' <<<"$1" || true; }
# -------------------------
# Key discovery (patched)
# -------------------------
# Find .gpg files that contain any fingerprint whose last 16 hex chars equal sig.
# Returns two outputs via named arrays: files_out (list of files) and fps_map (associative map file->comma-separated matching full fprs)
# Usage: find_keys_by_sig "$sig" files_out fps_map
find_keys_by_sig() {
local sig=$1
local -n files_out=$2
local -n fps_map=$3
files_out=()
declare -A tmpmap=()
while IFS= read -r keyfile; do
# collect all fprs in this file
local fprs
fprs=$(gpg --batch --no-tty --with-colons --show-keys "$keyfile" 2>/dev/null |
awk -F: '$1=="fpr" {print $10}' || true)
if [[ -z "$fprs" ]]; then
# fallback: human-readable greedy fingerprint extraction
fprs=$(gpg --batch --no-tty --show-keys "$keyfile" 2>/dev/null |
grep -oE '[0-9A-F]{40}' || true)
fi
[[ -z "$fprs" ]] && continue
local matched=""
while IFS= read -r fpr; do
[[ -z "$fpr" ]] && continue
if [[ "${fpr: -16}" == "$sig" ]]; then
if [[ -n "$matched" ]]; then
matched+=",${fpr}"
else
matched="$fpr"
fi
fi
done <<<"$fprs"
if [[ -n "$matched" ]]; then
files_out+=("$keyfile")
tmpmap["$keyfile"]="$matched"
fi
done < <(find "$APT_TRUST_DIR" -type f -name '*.gpg' -print 2>/dev/null || true)
# export map as csv-string values in associative array reference fps_map
for k in "${!tmpmap[@]}"; do
fps_map["$k"]="${tmpmap[$k]}"
done
}
# -------------------------
# Key expiration detection (patched)
# -------------------------
# key_is_expired: robustly check expiration for only the fprs listed in the fps_csv for the given keyfile.
# Returns 0 if expired (all matching pubs expired), 1 otherwise.
# Usage: key_is_expired "<keyfile>" "<fps_csv>"
key_is_expired() {
local keyfile=$1
local fps_csv=$2
local now
now=$(date +%s)
IFS=',' read -r -a want_fprs <<<"$fps_csv"
for i in "${!want_fprs[@]}"; do
want_fprs[$i]=$(tr '[:lower:]' '[:upper:]' <<<"${want_fprs[$i]}")
done
local seen_match=0 expired_match=0
# Primary: parse --with-colons (deterministic)
# pub parts: [0]=pub, [1]=flags, [6]=creation, [7]=expiry (epoch or 0)
local col_line pub_expiry=""
while IFS= read -r col_line; do
IFS=':' read -r -a parts <<<"$col_line"
case "${parts[0]}" in
pub)
pub_expiry="${parts[7]}"
# normalize zero to empty
[[ "$pub_expiry" == "0" ]] && pub_expiry=""
# If flags include 'e' (expired) treat as expired even if expiry empty
if [[ "${parts[1]}" == *e* ]]; then
pub_expiry=0
fi
;;
fpr)
local fpr="${parts[9]}"
fpr=$(tr '[:lower:]' '[:upper:]' <<<"$fpr")
for want in "${want_fprs[@]}"; do
if [[ "$fpr" == "$want" ]]; then
seen_match=1
if [[ -n "$pub_expiry" && "$pub_expiry" =~ ^[0-9]+$ ]]; then
if (( pub_expiry <= now )); then
expired_match=1
break 2
fi
fi
fi
done
;;
esac
done < <(gpg --batch --no-tty --with-colons --show-keys "$keyfile" 2>/dev/null || true)
# Secondary fallback: human-readable parsing if no match found via with-colons
if (( seen_match == 0 )); then
local publine="" line
while IFS= read -r line; do
if [[ $line =~ ^[[:space:]]*pub[[:space:]] ]]; then
publine="$line"
continue
fi
if grep -qE '[0-9A-Fa-f]{40}' <<<"$line"; then
local this_fpr
this_fpr=$(grep -oEi '[0-9A-Fa-f]{40}' <<<"$line" | head -n1 | tr '[:lower:]' '[:upper:]')
if [[ -n "$publine" ]]; then
for want in "${want_fprs[@]}"; do
if [[ "$this_fpr" == "$want" ]]; then
seen_match=1
local exp_date
exp_date=$(grep -oE 'expires:[[:space:]]*[0-9]{4}-[0-9]{2}-[0-9]{2}' <<<"$publine" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' || true)
if [[ -n "$exp_date" ]]; then
local exp_epoch
exp_epoch=$(date -d "$exp_date" +%s 2>/dev/null || true)
if [[ -n "$exp_epoch" && "$exp_epoch" -le "$now" ]]; then
expired_match=1
break 3
fi
fi
fi
done
publine=""
fi
fi
done < <(gpg --batch --no-tty --show-keys "$keyfile" 2>/dev/null || true)
fi
(( seen_match == 1 && expired_match == 1 )) && return 0
return 1
}
# -------------------------
# Safe renaming helpers
# -------------------------
# Create destination name by appending a suffix; if destination exists add timestamp.
make_dst_name() {
local src=$1; local suffix=$2
local dst="${src}${suffix}"
if [[ -e "$dst" ]]; then
dst="${src}.$(timestamp)${suffix}"
fi
printf '%s' "$dst"
}
# Rename a file safely and log; increments renamed_sources counter.
safe_rename() {
local src=$1
local dst
dst=$(make_dst_name "$src" ".orig")
if mv -- "$src" "$dst"; then
log "Renamed: $src → $dst"
renamed_sources=$((renamed_sources+1))
else
warn "Failed to rename: $src"
fi
}
# Rename a GPG key file safely and log; increments renamed_keys counter.
safe_rename_key() {
local key=$1
local dst
dst=$(make_dst_name "$key" "~")
if mv -- "$key" "$dst"; then
log "Renamed GPG key: $key → $dst"
renamed_keys=$((renamed_keys+1))
else
warn "Failed to rename GPG key: $key"
fi
}
# -------------------------
# Preconditions
# -------------------------
if [[ $EUID -ne 0 ]]; then
err "This script must be run as root."
err "Use sudo or run as root."
exit 2
fi
# -------------------------
# Run apt-get update and capture errors
# -------------------------
tmp_log=$(mktemp)
trap 'rm -f "$tmp_log"' EXIT
# Capture apt output; preserve errors but do not abort on non-zero
apt-get update 2>&1 | tee "$tmp_log" >/dev/null || true
# -------------------------
# Parse apt output for problematic lines
# -------------------------
# Use a temp file for filtered error lines to avoid process-substitution timing issues.
err_lines=$(mktemp)
trap 'rm -f "$tmp_log" "$err_lines"' EXIT
grep -Ei "$ERR_REGEX" "$tmp_log" > "$err_lines" || true
while IFS= read -r line; do
if [[ "$line" =~ ^Err: ]]; then
# Try to find the HTTP token on the line (last http* token)
current_url=$(awk '{for(i=NF;i>0;i--) if($i~/^http/) {print $i; exit}}' <<<"$line")
if [[ -n "${current_url:-}" ]] && is_http_url "$current_url"; then
url_to_sig["$current_url"]=""
else
current_url=""
fi
elif [[ "$line" =~ EXPKEYSIG ]]; then
local_sig=$(extract_sig "$line")
# only attach sig to a present valid current_url
if [[ -n "${current_url:-}" ]] && is_http_url "$current_url"; then
url_to_sig["$current_url"]="$local_sig"
fi
fi
done < "$err_lines"
# If no entries, exit early with same user message as before
if (( ${#url_to_sig[@]} == 0 )); then
log "No problematic repository errors detected."
exit 0
fi
# -------------------------
# Gather all source list files
# -------------------------
for pat in "${PATTERNS[@]}"; do
for f in "$SRC_DIR"/$pat; do
[[ -e "$f" ]] && all_src_files+=("$f")
done
done
[[ -f /etc/apt/sources.list ]] && all_src_files+=("/etc/apt/sources.list")
# -------------------------
# Process problematic URLs
# -------------------------
for url in "${!url_to_sig[@]}"; do
# Skip invalid keys (defensive)
[[ -z "${url:-}" ]] && continue
[[ ! $(is_http_url "$url" && printf true) ]] && continue
sig="${url_to_sig[$url]:-}"
if [[ -z "$sig" ]]; then
log "Renaming source files that reference URL without a valid release: $url"
for src in "${all_src_files[@]:-}"; do
if [[ -f "$src" ]] && grep -Fq "$url" "$src"; then
safe_rename "$src"
fi
done
else
log "Looking for expired GPG keys for signature $sig (URL: $url)"
# Updated call: collect files and per-file matching fprs
declare -a matching_key_files=()
declare -A matching_fps_map=()
find_keys_by_sig "$sig" matching_key_files matching_fps_map
if (( ${#matching_key_files[@]} > 0 )); then
for key in "${matching_key_files[@]}"; do
fps="${matching_fps_map[$key]}"
if key_is_expired "$key" "$fps"; then
safe_rename_key "$key"
else
log "GPG key not expired, skipping rename: $key"
fi
done
else
log "No matching GPG key found for signature $sig"
fi
fi
done
# -------------------------
# Final summary (keeps user-facing behaviour)
# -------------------------
if (( renamed_sources == 0 && renamed_keys == 0 )); then
echo "It looks good. No errors encountered."
else
log "All requested renames completed."
log "Renamed ${renamed_sources} source(s) and ${renamed_keys} key(s)."
fi
exit 0