From e255353d8db12f6e694ce298852bbe76c2f00ce1 Mon Sep 17 00:00:00 2001 From: bill-auger Date: Sat, 2 Jan 2021 18:13:39 -0500 Subject: [parabola-keys]: refactor - handle sub-keys - display hacker logins --- src/maintenance-tools/parabola-keys | 339 ++++++++++++++++++++++++++++-------- 1 file changed, 262 insertions(+), 77 deletions(-) diff --git a/src/maintenance-tools/parabola-keys b/src/maintenance-tools/parabola-keys index 76aa221..4ca34bf 100755 --- a/src/maintenance-tools/parabola-keys +++ b/src/maintenance-tools/parabola-keys @@ -1,107 +1,292 @@ #!/bin/bash -readonly KEYS_FILE=/usr/share/pacman/keyrings/parabola-trusted + +# 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 ) +readonly SHOULD_SHOW_ALL=$( [[ "$1" == '--all' ]] && echo 1 || echo 0 ) ; (( $# > 1 )) && shift ; readonly CHROOT=$( [[ "$1" == '--chroot' ]] && echo $2 ) -readonly KEYS=$(cat $KEYS_FILE) -readonly JOIN_CHAR='~' -# readonly EMAIL_REGEX='.*key \([^ ,]*\), .*' -# readonly KEY_REGEX='.*key \([^ ,]*\), .*' -readonly EXPIRY_REGEX='.* expires: \([0-9-]*\).*' +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 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 ; } -declare -A all_keys -declare -A warning_keys -declare -A valid_keys -declare -A expired_keys -declare -A revoked_keys +## helpers ## -FetchKey() # (fingerprint) +ParseExpiry() # (key_data) { - gpg --batch --search-keys $1 2> /dev/null | tr "\n" "${JOIN_CHAR}" | sed -E 's|^\([0-9+]\)\s+||' + 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} } -ParseExpiry() # (key_data) +ParseKeyData() # (login key_id key_data) { - expiry=$(echo $1 | grep 'expires:' | sed "s|.*${EXPIRY_REGEX}|\1|") + 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}'" - [ "${expiry} " ] && echo ${expiry} || echo 'EXPIRY_INFINITE' + 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 } -IsValid() # (key_data) +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) { - echo $1 | grep -Ev '(expired)|(revoked)' > /dev/null + 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) { - echo $1 | grep -E '(expired)' > /dev/null + local key_data="$1" + + grep -E '(expired)' <<<${key_data} > /dev/null } IsRevoked() # (key_data) { - echo $1 | grep -E '(revoked)' > /dev/null + local key_data="$1" + + grep -E '(revoked)' <<<${key_data} > /dev/null } -# run in chroot -if [[ -d "$CHROOT" ]] -then sudo cp ${BASH_SOURCE} $CHROOT/ - sudo chroot $CHROOT/ ./$(basename ${BASH_SOURCE}) - exit -fi +IsUntrusted() # (key_data) +{ + local key_data="$1" -# collect results -echo -n "($(echo $KEYS | wc -w)) keys to consider " -for key in ${KEYS} -do [[ "${key%%:*}" != ${AUTOBUILDER_KEY} ]] && echo -n '.' || continue - - # fetch and parse key data - key=${key%%:*} - key_data="$(FetchKey ${key})" - - # detect expiry warning period - expiry=$(ParseExpiry "${key_data}") - expiry_ts=$(date --date ${expiry} +%s 2> /dev/null) - expiry_duration=$(( ${expiry_ts} - $NOW )) - (( ${expiry_duration} <= ${WARNING_DURATION} )) && \ - (( ${expiry_duration} > 0 )) && should_warn=1 || \ - should_warn=0 - - # cache key data (mutually exclusive states) - all_keys[${key}]="${key_data}" - (( ${should_warn} )) && warning_keys[${key}]="${expiry}" && continue - IsValid "${key_data}" && valid_keys[${key}]="${expiry}" && continue - IsExpired "${key_data}" && expired_keys[${key}]="${expiry}" && continue - IsRevoked "${key_data}" && revoked_keys[${key}]="${expiry}" && continue -done ; echo ; - -# display results -if (( ${#valid_keys[@]} * ${SHOULD_SHOW_ALL} )) -then echo -e "\n== valid_keys ==\n" - for key in "${!valid_keys[@]}" - do echo ${all_keys[${key}]} | tr "${JOIN_CHAR}" "\n" - done -fi -if (( ${#warning_keys[@]} )) -then echo -e "\n== warning_keys ==\n" - for key in "${!warning_keys[@]}" - do echo ${all_keys[${key}]} | tr "${JOIN_CHAR}" "\n" - done -fi -if (( ${#expired_keys[@]} )) -then echo -e "\n== expired_keys ==\n" - for key in "${!expired_keys[@]}" - do echo ${all_keys[${key}]} | tr "${JOIN_CHAR}" "\n" - done -fi -if (( ${#revoked_keys[@]} )) -then echo -e "\n== revoked_keys ==\n" - for key in "${!revoked_keys[@]}" - do echo ${all_keys[${key}]} | tr "${JOIN_CHAR}" "\n" - done + ! 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 -- cgit v1.2.2