264 lines
6.5 KiB
Bash
Executable file
264 lines
6.5 KiB
Bash
Executable file
#!/bin/sh
|
|
#
|
|
# $FreeBSD$
|
|
#
|
|
|
|
#
|
|
# This script is intended to sanity-check PGP keys used by folks with
|
|
# @FreeBSD.org email addresses. The checks are intended to be derived
|
|
# from the information at
|
|
# <https://we.riseup.net/riseuplabs+paow/openpgp-best-practices#openpgp-key-checks>
|
|
#
|
|
|
|
# make sure we are in a well known language
|
|
LANG=C
|
|
|
|
\unalias -a
|
|
|
|
progname=${0##*/}
|
|
|
|
# Print an informational message
|
|
info() {
|
|
echo "$@" >&2
|
|
}
|
|
|
|
# Print a warning message
|
|
warning() {
|
|
echo "WARNING: $@" >&2
|
|
}
|
|
|
|
# Print an error message and exit
|
|
error() {
|
|
echo "ERROR: $@" >&2
|
|
exit 1
|
|
}
|
|
|
|
# Print usage message and exit
|
|
usage() {
|
|
echo "usage: ${progname} [user] [keyid ...]\n" >&2
|
|
exit 1
|
|
}
|
|
|
|
# Look for gpg
|
|
gpg=$(which gpg)
|
|
if [ $? -gt 0 -o -z "${gpg}" -o ! -x "${gpg}" ] ; then
|
|
error "Cannot find gpg"
|
|
fi
|
|
|
|
# Set up our internal default gpg invocation options
|
|
_gpg() {
|
|
${gpg} \
|
|
--display-charset utf-8 \
|
|
--no-greeting \
|
|
--no-secmem-warning \
|
|
--keyid-format long \
|
|
--list-options no-show-uid-validity \
|
|
"$@"
|
|
}
|
|
|
|
# Look up key by key ID
|
|
getkeybyid() {
|
|
_gpg --with-colons --list-keys "$1" 2>/dev/null | awk -F: \
|
|
'$5 ~ /^\([0-9A-F]{8}\)?'"$1"'$/i && $12 ~ /ESC/ { print $5 }'
|
|
}
|
|
|
|
# Look up key by email
|
|
getkeybyemail() {
|
|
_gpg --with-colons --list-keys "$1" 2>/dev/null | awk -F: \
|
|
'$10 ~ /<'"$1"'>/i && $12 ~ /ESC/ { print $5 }'
|
|
}
|
|
|
|
# The first command-line argument can be a user name or a key ID.
|
|
if [ $# -gt 0 ] ; then
|
|
id="$1"
|
|
shift
|
|
else
|
|
id=$(id -nu)
|
|
warning "No argument specified, calculating user ID"
|
|
fi
|
|
|
|
# Now let's try to figure out what kind of thing we have as an ID.
|
|
# We'll check for a keyid first, as it's readily distinguishable
|
|
# from other things, but if we see that we have one, we push it back
|
|
# onto the argument list for later processing (becasue we may have
|
|
# been given a list of keyods).
|
|
if echo "${id}" | egrep -q '^[0-9A-F]{16}$'; then
|
|
id_type="keyid"
|
|
set -- "${id}" $@
|
|
elif echo "${id}" | egrep -q '^[0-9A-F]{8}$'; then
|
|
id_type="keyid"
|
|
set -- "${id}" $@
|
|
elif echo "${id}" | egrep -iq '^[a-z][-0-9a-z_]*@([-0-9a-z]+\.)[-0-9a-z]+$'; then
|
|
id_type="email"
|
|
email="${id}"
|
|
elif echo "${id}" | egrep -iq '^[a-z][-0-9a-z_]*$'; then
|
|
id_type="login"
|
|
login="${id}"
|
|
email="${id}@FreeBSD.org"
|
|
else
|
|
error "Cannot recognize type of ${id} (keyid, login, or email)"
|
|
fi
|
|
|
|
if [ $# -ne 0 ] ; then
|
|
# Verify the keys that were specified on the command line
|
|
for arg ; do
|
|
case $(expr "${arg}" : '^[0-9A-Fa-f]\{8,16\}$') in
|
|
8)
|
|
warning "${arg}: recommend using 16-digit keyid"
|
|
;&
|
|
16)
|
|
keyid=$(getkeybyid "${arg}")
|
|
if [ -n "${keyid}" ] ; then
|
|
keyids="${keyids} ${keyid}"
|
|
else
|
|
warning "${arg} not found"
|
|
fi
|
|
;;
|
|
*)
|
|
warning "${arg} does not appear to be a valid key ID"
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
else
|
|
# Search for keys by freebsd.org email
|
|
keyids=$(getkeybyemail "${email}")
|
|
case $(echo "${keyids}" | wc -w) in
|
|
0)
|
|
error "no keys found for ${email}"
|
|
;;
|
|
1)
|
|
;;
|
|
*)
|
|
warning "Multiple keys found for <${email}>; checking all."
|
|
warning "If this is not what you want, specify a key ID" \
|
|
"on the command line."
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
# :(
|
|
if [ -z "${keyids}" ] ; then
|
|
error "no valid keys were found"
|
|
fi
|
|
|
|
# add a problem report to the list of problems with this key
|
|
badkey() {
|
|
key_problems=" ${key_problems}$@
|
|
"
|
|
}
|
|
|
|
exitstatus=0
|
|
|
|
# Check the keys
|
|
for key in ${keyids} ; do
|
|
# no problems found yet
|
|
key_problems=""
|
|
|
|
IFS_save="${IFS}"
|
|
key_info=$( ${gpg} --no-secmem-warning --export-options export-minimal --export ${key} \
|
|
| ${gpg} --no-secmem-warning --list-packets )
|
|
# primary keys should be RSA or DSA-2
|
|
IFS=""
|
|
version=$( echo $key_info | \
|
|
awk '$1 == "version" && $3 == "algo" {sub(",", "", $2); print $2; exit 0}' )
|
|
IFS="${IFS_save}"
|
|
if [ $version -lt 4 ]; then
|
|
badkey "This key is a deprecated version $version key!"
|
|
fi
|
|
|
|
IFS=""
|
|
algonum=$( echo $key_info | \
|
|
awk '$1 == "version" && $3 == "algo" {sub(",", "", $4); print $4; exit 0}' )
|
|
IFS="${IFS_save}"
|
|
case ${algonum} in
|
|
"1") algo="RSA" ;;
|
|
"17") algo="DSA" ;;
|
|
"18") algo="ECC" ;;
|
|
"19") algo="ECDSA" ;;
|
|
*) algo="*UNKNOWN*" ;;
|
|
esac
|
|
|
|
IFS=""
|
|
bitlen=$( echo $key_info | \
|
|
awk -F : '$1 ~ "pkey" { gsub("[^0-9]*","", $2); print $2; exit 0}' )
|
|
IFS="${IFS_save}"
|
|
echo "key ${key}: ${algo}, ${bitlen} bits"
|
|
case ${algo} in
|
|
RSA) ;;
|
|
DSA) if [ "${bitlen}" -le 1024 ]; then \
|
|
badkey "DSA, but not DSA-2"; \
|
|
fi ;;
|
|
*) badkey "non-preferred algorithm"
|
|
esac
|
|
|
|
# self-signatures must not use MD5 or SHA1
|
|
IFS=""
|
|
sig_algonum=$( echo $key_info | \
|
|
awk '$1 == "digest" && $2 == "algo" {sub(",", "", $3); print $3; exit 0}' )
|
|
IFS="${IFS_save}"
|
|
case sig_algonum in
|
|
1) sigs="MD5";;
|
|
2) sigs="SHA1";;
|
|
3) sigs="RIPEMD160";;
|
|
8) sigs="SHA256";;
|
|
9) sigs="SHA384";;
|
|
10) sigs="SHA512";;
|
|
11) sigs="SHA224";;
|
|
*)
|
|
esac
|
|
for sig in ${sigs}; do
|
|
if [ "${sig}" = "MD5" -o "${sig}" = "SHA1" ]; then
|
|
badkey "self-signature ${sig}"
|
|
fi
|
|
done
|
|
|
|
# digest algo pref must include at least one member of SHA-2
|
|
# at a higher priority than both MD5 and SHA1
|
|
IFS=""
|
|
algopref=$( echo $key_info | \
|
|
awk -F : '$1 ~ "pref-hash-algos" {gsub("[^ 0-9]", "", $2); print $2; exit 0}' )
|
|
IFS="${IFS_save}"
|
|
# if 3, 2, or 1 are before 11, 10, 9, or 8, then
|
|
set -- ${algopref}
|
|
if [ $1 -lt 4 ]; then
|
|
badkey "algorithm prefs do not have SHA-2 higher than MD5 or SHA1"
|
|
fi
|
|
|
|
# primary keys should have an expiration date at least a year
|
|
# in the future to make them worth committing, but no more
|
|
# than three years in the future
|
|
expires=$( _gpg --list-keys ${key} | \
|
|
awk "/$keyid .*expires:/ {sub(\"[^-0-9]\", \"\", \$NF); print \$NF; exit 0}" )
|
|
if [ -z "${expires}" ]; then
|
|
badkey "this key does not expire"
|
|
else
|
|
expires_s=$( date -jf "%F" "+%s" "${expires}" )
|
|
now_s=$( date "+%s" )
|
|
# 86400 == # seconds in a normal day
|
|
expire_int_d=$(( ( ${expires_s} - ${now_s} ) / 86400 ))
|
|
exp_min=$(( 1 * 365 )) # Min expiry time is 1 year
|
|
exp_max=$(( 3 * 365 + 1 )) # Max expiry time is 3 years
|
|
# We add 1 day because in a 3-year
|
|
# period, probability of a leap day
|
|
# is 297/400, about 0.74250
|
|
if [ ${expire_int_d} -lt ${exp_min} ]; then
|
|
badkey "Key $key expires in less than 1 year ($expire_int_d days)"
|
|
fi
|
|
if [ ${expire_int_d} -gt ${exp_max} ]; then
|
|
badkey "Key $key expires in more than 3 years ($expire_int_d days)"
|
|
fi
|
|
fi
|
|
|
|
# report problems
|
|
if [ -z "${key_problems}" ]; then
|
|
echo " key okay, ${key} meets minimal requirements" >&2
|
|
else
|
|
exitstatus=1
|
|
echo " ** problems found:" >&2
|
|
echo "${key_problems}" >&2
|
|
echo " ** key ${key} should not be used!"
|
|
fi
|
|
echo
|
|
done
|
|
exit ${exitstatus}
|