#!/bin/bash # parabola-keys - detect problems and inconsistencies # # This file is part of Parabola Libretools. # Copyright 2020-2021 bill-auger # # Libretools is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Libretools is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Libretools. If not, see . # # # DESCRIPTION: # # this script depends on the 'parabola-hackers' package # TODO: it should probably be moved into the 'parabola-hackers' package # # if any problems are reported, other than 'WarningKeys' or 'OutdatedKeys', # the 'parabola-keyring' package should be rebuilt with high-priority # 'OutdatedKeys' may or may not indicate a probelm, # and require manual inspection, as these have bypassed all other checks # # the 'parabola-keyring' keys are loaded/refreshed into the local user GPG keyring, # directly from a keyserver, independent of the 'pacman-keyring' keyring # then compared to the state of the pristine 'pacman-keyring' keyring # in that way, edge cases can be detected, # which could be resolved with `pacman-key --refresh`, # but which may be blocking installs/upgrades otherwise # in that way, problems are detected, which would block some installs/upgrades # and also certain edge cases, which could be resolved with `pacman-key --refresh`, # such as expiry extension after the last 'parabola-keyring' package build, # but which may nonetheless be blocking installs/upgrades for some users # optimal accuracy depends on a pristine system pacman keyring (/etc/pacman.d/gnupg/) # (ie: it _not_ been refreshed explicitly, since the last 'parabola-keyring' upgrade) # # # OPTIONS: # # --all # list all keys, including those without problems ('ValidKeys') # # --chroot # check the 'parabola-keyring' of a chroot system # the chroot system must be already mounted, or otherwise accessible # readonly HACKER_KEYS_SCRIPT=/usr/lib/parabola-hackers/pgp-list-keyids readonly KEYSERVER=hkp://hkps.pool.sks-keyservers.net:11371/ readonly WARNING_N_DAYS=30 readonly AUTOBUILDER_KEY='D3EAD7F9D076EB9AF650149DA170D6A0B669E21A' readonly SHOULD_SHOW_ALL=$( [[ "$1" == '--all' ]] && echo 1 || echo 0 ) ; (( $# > 1 )) && shift ; readonly CHROOT=$( [[ "$1" == '--chroot' ]] && echo $2 ) readonly JOIN_CHAR='=' readonly INFINITE_EXPIRY='EXPIRY_INFINITE' readonly HACKER_KEY_REGEX="([^${JOIN_CHAR}]+)${JOIN_CHAR}([^${JOIN_CHAR}]+)" readonly KEYDATA_EXPIRY_1_REGEX="([^=]+)${JOIN_CHAR}([0-9A-F]{40})${JOIN_CHAR}([0-9]{4}-[0-9]{2}-[0-9]{2})" readonly KEYDATA_EXPIRY_2_REGEX="([^=]+)${JOIN_CHAR}([0-9A-F]{40})${JOIN_CHAR}(${INFINITE_EXPIRY})" readonly EXPIRY_REGEX='^.* \[expire[ds]: ([0-9]{4}-[0-9]{2}-[0-9]{2})\]$' readonly TRUSTED_REGEX='^uid \[ full \]' readonly GPG_CMD="gpg --keyserver ${KEYSERVER}" readonly GPG_LIST_OPTIONS='--list-options show-unusable-subkeys --with-subkey-fingerprint' readonly PACMAN_LIST_OPTIONS="--homedir /etc/pacman.d/gnupg ${GPG_LIST_OPTIONS}" readonly NOW=$(date +%s) readonly WARNING_DURATION=$(( 86400 * WARNING_N_DAYS )) readonly CHROOT_ERR_MSG="ERROR: specified chroot does not exist: '${CHROOT}'" readonly HACKER_KEY_ERR_MSG="ERROR: no key data - check ${HACKER_KEYS_SCRIPT}" readonly HACKER_KEYS=( $(${HACKER_KEYS_SCRIPT} | grep -v ${AUTOBUILDER_KEY} | tr ' ' ${JOIN_CHAR}) ) # testing examples below: # readonly HACKER_KEYS=("trusted/aurelien${JOIN_CHAR}560B3DEC2F13E822ACED475B2EC52AC76AEEB6A0") # EXPIRY_INFINITE,no-subkey # readonly HACKER_KEYS=("trusted/bill-auger${JOIN_CHAR}FBCC5AD7421197B7ABA72853908710913E8C7778") # rolling expiry,subkey # readonly HACKER_KEYS=("trusted/oaken-source${JOIN_CHAR}BFA8008A8265677063B11BF47171986E4B745536") # rolling expiry,no-signing-subkey # readonly HACKER_KEYS=("trusted/freemor${JOIN_CHAR}17446CB76E250EE809DFE1F2A7DC2FC7EBC8F713") declare -A OutdatedKeys declare -A WarningKeys declare -A RevokedKeys declare -A ExpiredKeys declare -A UntrustedKeys declare -A ValidKeys ## logging ## readonly DEBUG=0 DBG() { (( DEBUG )) && LOG "$@" ; } LOG() { echo -e "$@" >&2 ; } ## helpers ## ParseExpiry() # (key_data) { local key_data="$1" local expiry=$(grep -E "${EXPIRY_REGEX}" <<<${key_data} | sed -E "s|${EXPIRY_REGEX}|\1|") [[ -n "${expiry}" ]] && echo ${expiry} || echo ${INFINITE_EXPIRY} } ParseKeyData() # (login key_id key_data) { local login=$1 local key_id=$2 local key_data="$3" local master_key_data="$(grep --max-count=1 -A 1 -E '^pub ' <<<${key_data})" local subkey_data="$( grep --max-count=1 -A 1 -E '^sub ' <<<${key_data})" local master_key_id=$(tail -n 1 <<<${master_key_data} | tr -d ' ') local subkey_id=$( tail -n 1 <<<${subkey_data} | tr -d ' ') local master_expiry=$(ParseExpiry "${master_key_data}") local subkey_expiry=$(ParseExpiry "${subkey_data}" ) DBG "\nParseKeyData() key_id='${key_id}'" # DBG "ParseKeyData() key_data='${key_data}'" # DBG "ParseKeyData() master_key_data='${master_key_data}'" # DBG "ParseKeyData() subkey_data='${subkey_data}'" DBG "ParseKeyData() master_key_id='${master_key_id}'" DBG "ParseKeyData() subkey_id='${subkey_id}'" DBG "ParseKeyData() master_expiry='${master_expiry}'" DBG "ParseKeyData() subkey_expiry='${subkey_expiry}'" if [[ -z "${subkey_data}" ]] then echo "${login}${JOIN_CHAR}${master_key_id}${JOIN_CHAR}${master_expiry}" else echo "${login}${JOIN_CHAR}${master_key_id}${JOIN_CHAR}${master_expiry} ${login}${JOIN_CHAR}${subkey_id}${JOIN_CHAR}${subkey_expiry}" fi } LoadKeyData() # (login key_id "list_options") { local login=$1 local key_id=$2 local list_options="$3" [[ "${list_options}" == "${GPG_LIST_OPTIONS}" ]] && ${GPG_CMD} --recv-keys ${key_id} &> /dev/null ParseKeyData ${login} ${key_id} "$(${GPG_CMD} ${list_options} --list-keys ${key_id} 2> /dev/null)" } RawKeysData() # (key_id) { local key_id=$1 pacman-key --list-keys ${key_id} 2> /dev/null } ShouldWarn() # (expiry) { local expiry=$1 local expiry_ts=$(date --date ${expiry} +%s 2> /dev/null) local expiry_duration=$(( expiry_ts - NOW )) [[ ${expiry_duration} -gt 0 && ${expiry_duration} -lt ${WARNING_DURATION} ]] } IsExpired() # (key_data) { local key_data="$1" grep -E '(expired)' <<<${key_data} > /dev/null } IsRevoked() # (key_data) { local key_data="$1" grep -E '(revoked)' <<<${key_data} > /dev/null } IsUntrusted() # (key_data) { local key_data="$1" ! grep -E "${TRUSTED_REGEX}" <<<${key_data} > /dev/null } IsValid() # (key_data) { local key_data="$1" grep -Ev '(expired)|(revoked)' <<<${key_data} > /dev/null } ## business ## ProcessHackerKey() # (hacker_data) { ! [[ "$1" =~ ^${HACKER_KEY_REGEX}$ ]] && LOG "${HACKER_KEY_ERR_MSG}" && return 1 local login=${BASH_REMATCH[1]} local key_id=${BASH_REMATCH[2]} DBG "\nProcessHackerKey() login=${login} key_id=${key_id}" # fetch, parse, and cache key data local gpg_keys_data="$( LoadKeyData ${login} ${key_id} "${GPG_LIST_OPTIONS}" )" local pacman_keys_data="$(LoadKeyData ${login} ${key_id} "${PACMAN_LIST_OPTIONS}")" local raw_keys_data="$( RawKeysData ${key_id} )" # process key data if [[ "${gpg_keys_data}" == "${pacman_keys_data}" ]] then # process both master and subkey, if subkey in hackers.git # TODO: how to know if subkey in hackers.git local keys_data="${pacman_keys_data}" local key_data local expiry for key_data in ${keys_data} do # load key data if [[ "${key_data}" =~ ^${KEYDATA_EXPIRY_1_REGEX}$ ]] || [[ "${key_data}" =~ ^${KEYDATA_EXPIRY_2_REGEX}$ ]] then login=${BASH_REMATCH[ 1]} key_id=${BASH_REMATCH[2]} expiry=${BASH_REMATCH[3]} else login='' key_id='' expiry='' LOG "ERROR: parsing key: ${key_id}" fi [[ -n "${expiry}" ]] && DBG "ProcessHackerKey() keys_data=${keys_data}\nProcessHackerKey() key_id=${key_id}\nProcessHackerKey() expiry=${expiry}" || DBG "ProcessHackerKey() key_data=${key_data}" [[ -n "${expiry}" ]] || continue # process key data (mutually exclusive states) ShouldWarn ${expiry} && WarningKeys[${login}]="${key_id} ${expiry}" && continue IsRevoked "${keys_data}" && RevokedKeys[${login}]="${key_id} ${expiry}" && continue IsExpired "${keys_data}" && ExpiredKeys[${login}]="${key_id} ${expiry}" && continue IsUntrusted "${raw_keys_data}" && UntrustedKeys[${login}]="${key_id} ${expiry}" && continue IsValid "${keys_data}" && ValidKeys[${login}]="${key_id} ${expiry}" done else OutdatedKeys[${login}]="${gpg_keys_data}\n${pacman_keys_data}" DBG "ProcessHackerKey() OutdatedKeys gpg_keys_data =${gpg_keys_data}\nProcessHackerKey() OutdatedKeys pacman_keys_data=${pacman_keys_data}" fi } main() { # collect results echo -n "(${#HACKER_KEYS[@]}) keys to consider " for hacker_key in ${HACKER_KEYS[@]} do echo -n '.' && ProcessHackerKey ${hacker_key} || : done ; echo ; # display results if (( ${#ValidKeys[@]} * SHOULD_SHOW_ALL )) then echo -e "\n\n== ValidKeys ==\n" for login in "${!ValidKeys[@]}" ; do echo "${login} ${ValidKeys[${login}]}" ; done ; fi if (( ${#OutdatedKeys[@]} )) then echo -e "\n\n== OutdatedKeys ==\n" for login in "${!OutdatedKeys[@]}" ; do echo -e "${OutdatedKeys[${login}]}" ; done ; fi if (( ${#WarningKeys[@]} )) then echo -e "\n\n== WarningKeys ==\n" for login in "${!WarningKeys[@]}" ; do echo "${login} ${WarningKeys[${login}]}" ; done ; fi if (( ${#RevokedKeys[@]} )) then echo -e "\n\n== RevokedKeys ==\n" for login in "${!RevokedKeys[@]}" ; do echo "${login} ${RevokedKeys[${login}]}" ; done ; fi if (( ${#ExpiredKeys[@]} )) then echo -e "\n\n== ExpiredKeys ==\n" for login in "${!ExpiredKeys[@]}" ; do echo "${login} ${ExpiredKeys[${login}]}" ; done ; fi if (( ${#UntrustedKeys[@]} )) then echo -e "\n\n== UntrustedKeys ==\n" for login in "${!UntrustedKeys[@]}" ; do echo "${login} ${UntrustedKeys[${login}]}" ; done ; fi } ## main entry ## set -e # re-run in chroot if [[ -z "${CHROOT}" ]] then main elif [[ -d "${CHROOT}" ]] then sudo cp ${BASH_SOURCE} ${CHROOT}/ sudo chroot ${CHROOT}/ ./$(basename ${BASH_SOURCE}) else LOG "${CHROOT_ERR_MSG}" fi