2020-11-12 22:56:38 +00:00
|
|
|
#! /usr/bin/env bash
|
|
|
|
set -e
|
|
|
|
|
2020-11-13 02:31:26 +00:00
|
|
|
MIN_BASH_VERSION=4
|
|
|
|
MIN_GPG_VERSION=2.2
|
|
|
|
MIN_OPENSSL_VERSION=1.1
|
2020-11-14 01:15:00 +00:00
|
|
|
MIN_GETOPT_VERSION=2.33
|
2020-11-13 02:31:26 +00:00
|
|
|
|
2020-11-16 10:40:04 +00:00
|
|
|
## Private Functions
|
|
|
|
|
|
|
|
### Bail with error message
|
2020-11-12 22:56:38 +00:00
|
|
|
die() {
|
|
|
|
echo "$@" >&2
|
|
|
|
exit 1
|
|
|
|
}
|
|
|
|
|
2020-11-16 10:40:04 +00:00
|
|
|
### Bail and instruct user on missing package to install for their platform
|
2020-11-14 01:15:00 +00:00
|
|
|
die_pkg() {
|
|
|
|
local package=${1?}
|
|
|
|
local version=${2?}
|
|
|
|
local install_cmd
|
|
|
|
case "$OSTYPE" in
|
|
|
|
linux*)
|
|
|
|
if command -v "apt" >/dev/null; then
|
|
|
|
install_cmd="apt install ${package}"
|
|
|
|
elif command -v "yum" >/dev/null; then
|
|
|
|
install_cmd="yum install ${package}"
|
|
|
|
elif command -v "pacman" >/dev/null; then
|
|
|
|
install_cmd="pacman -Ss ${package}"
|
|
|
|
elif command -v "nix-env" >/dev/null; then
|
|
|
|
install_cmd="nix-env -i ${package}"
|
|
|
|
fi
|
|
|
|
;;
|
|
|
|
bsd*) install_cmd="pkg install ${package}" ;;
|
|
|
|
darwin*) install_cmd="port install ${package}" ;;
|
|
|
|
*) die "Error: Your operating system is not supported" ;;
|
|
|
|
esac
|
|
|
|
echo "Error: ${package} ${version}+ does not appear to be installed." >&2
|
|
|
|
[ ! -z "$install_cmd" ] && printf "Try: \`${install_cmd}\`" >&2
|
|
|
|
exit 1
|
|
|
|
}
|
|
|
|
|
2020-11-16 10:40:04 +00:00
|
|
|
### Check if actual binary version is >= minimum version
|
2020-11-13 02:31:26 +00:00
|
|
|
check_version(){
|
2020-11-14 01:15:00 +00:00
|
|
|
local pkg="${1?}"
|
|
|
|
local have="${2?}"
|
|
|
|
local need="${3?}"
|
|
|
|
[[ "$have" == "$need" ]] && return 0
|
2020-11-13 02:31:26 +00:00
|
|
|
local IFS=.
|
2020-11-14 01:15:00 +00:00
|
|
|
local i ver1=($have) ver2=($need)
|
2020-11-13 02:31:26 +00:00
|
|
|
for ((i=${#ver1[@]}; i<${#ver2[@]}; i++));
|
|
|
|
do ver1[i]=0;
|
|
|
|
done
|
|
|
|
for ((i=0; i<${#ver1[@]}; i++)); do
|
|
|
|
[[ -z ${ver2[i]} ]] && ver2[i]=0
|
|
|
|
((10#${ver1[i]} > 10#${ver2[i]})) && return 0
|
2020-11-14 01:15:00 +00:00
|
|
|
((10#${ver1[i]} < 10#${ver2[i]})) && die_pkg "${pkg}" "${need}"
|
2020-11-13 02:31:26 +00:00
|
|
|
done
|
|
|
|
}
|
|
|
|
|
2020-11-16 10:40:04 +00:00
|
|
|
### Check if required binaries are installed at appropriate versions
|
2020-11-13 02:31:26 +00:00
|
|
|
check_tools(){
|
|
|
|
if [ -z "${BASH_VERSINFO}" ] \
|
|
|
|
|| [ -z "${BASH_VERSINFO[0]}" ] \
|
|
|
|
|| [ ${BASH_VERSINFO[0]} -lt ${MIN_BASH_VERSION} ]; then
|
2020-11-14 01:15:00 +00:00
|
|
|
die_pkg "bash" "${MIN_BASH_VERSION}"
|
2020-11-13 02:31:26 +00:00
|
|
|
fi
|
2020-11-12 23:02:19 +00:00
|
|
|
for cmd in "$@"; do
|
2020-11-13 02:31:26 +00:00
|
|
|
command -v "$1" >/dev/null || die "Error: $cmd not found"
|
|
|
|
case $cmd in
|
|
|
|
gpg)
|
|
|
|
version=$(gpg --version | head -n1 | cut -d" " -f3)
|
2020-11-14 01:15:00 +00:00
|
|
|
check_version "gnupg" "${version}" "${MIN_GPG_VERSION}"
|
2020-11-13 02:31:26 +00:00
|
|
|
;;
|
|
|
|
openssl)
|
|
|
|
version=$(openssl version | cut -d" " -f2 | sed 's/[a-z]//g')
|
|
|
|
check_version "openssl" "${version}" "${MIN_OPENSSL_VERSION}"
|
|
|
|
;;
|
2020-11-14 01:15:00 +00:00
|
|
|
getopt)
|
|
|
|
version=$(getopt --version | cut -d" " -f4 | sed 's/[a-z]//g')
|
|
|
|
check_version "getopt" "${version}" "${MIN_GETOPT_VERSION}"
|
|
|
|
;;
|
2020-11-13 02:31:26 +00:00
|
|
|
esac
|
2020-11-12 23:02:19 +00:00
|
|
|
done
|
|
|
|
}
|
|
|
|
|
2020-11-16 10:40:04 +00:00
|
|
|
### Handle different implementations of mktemp across platforms
|
2020-11-13 02:31:26 +00:00
|
|
|
get_temp(){
|
|
|
|
echo "$(
|
|
|
|
mktemp \
|
|
|
|
--quiet \
|
|
|
|
--directory \
|
|
|
|
-t "$(basename "$0").XXXXXX" 2>/dev/null
|
|
|
|
|| mktemp \
|
|
|
|
--quiet \
|
|
|
|
--directory
|
|
|
|
)"
|
|
|
|
}
|
|
|
|
|
2020-11-16 10:40:04 +00:00
|
|
|
### Get files that will be added to the manifest for signing
|
|
|
|
### Use git if available, else fall back to find
|
2020-11-13 03:24:37 +00:00
|
|
|
get_files(){
|
|
|
|
if command -v git >/dev/null; then
|
|
|
|
git ls-files | grep -v ".${PROGRAM}"
|
|
|
|
else
|
|
|
|
find . \
|
|
|
|
-type f \
|
|
|
|
-not -path "./.git/*" \
|
|
|
|
-not -path "./.${PROGRAM}/*"
|
|
|
|
fi
|
2020-11-12 22:56:38 +00:00
|
|
|
}
|
|
|
|
|
2020-11-16 11:17:50 +00:00
|
|
|
get_signer(){
|
|
|
|
local fingerprint="${1?}"
|
|
|
|
gpg \
|
|
|
|
--list-keys \
|
|
|
|
--with-colons "${fingerprint}" 2>&1 \
|
|
|
|
| awk -F: '$1 == "uid" {print $10}' \
|
|
|
|
| head -n1
|
|
|
|
}
|
|
|
|
|
2020-11-16 10:40:04 +00:00
|
|
|
### Verify a file has 0-N unique valid detached signatures
|
|
|
|
### Optionally verify all signatures belong to keys in gpg alias group
|
2020-11-12 22:56:38 +00:00
|
|
|
verify_file() {
|
2020-11-14 01:15:00 +00:00
|
|
|
[ $# -eq 3 ] || die "Usage: verify_file <threshold> <group> <file>"
|
2020-11-13 03:24:37 +00:00
|
|
|
local threshold="${1}"
|
2020-11-13 22:40:49 +00:00
|
|
|
local group="${2}"
|
|
|
|
local filename="${3}"
|
|
|
|
local group_config=""
|
2020-11-13 02:31:26 +00:00
|
|
|
local sig_count=0
|
|
|
|
local seen_fingerprints=""
|
|
|
|
local fingerprint
|
|
|
|
local signer
|
2020-11-13 22:40:49 +00:00
|
|
|
|
2020-11-16 10:40:04 +00:00
|
|
|
if [ ! -z "$group" ]; then
|
|
|
|
group_config="$( \
|
|
|
|
gpg --with-colons --list-config group \
|
|
|
|
| grep -i "^cfg:group:${group}:" \
|
|
|
|
)" || die "Error: group \"${group}\" not found in ~/.gnupg/gpg.conf"
|
|
|
|
fi
|
2020-11-13 22:40:49 +00:00
|
|
|
|
2020-11-12 22:56:38 +00:00
|
|
|
for sig_filename in "${filename%.*}".*.asc; do
|
|
|
|
gpg --verify "${sig_filename}" "${filename}" >/dev/null 2>&1 || {
|
|
|
|
echo "Invalid signature: ${sig_filename}";
|
|
|
|
exit 1;
|
|
|
|
}
|
|
|
|
fingerprint=$( \
|
|
|
|
gpg --list-packets "${sig_filename}" \
|
|
|
|
| grep keyid \
|
|
|
|
| sed 's/.*keyid //g'
|
|
|
|
)
|
2020-11-16 11:17:50 +00:00
|
|
|
signer=$( get_signer "${fingerprint}" )
|
2020-11-13 22:40:49 +00:00
|
|
|
|
|
|
|
[[ "${seen_fingerprints}" == *"${fingerprint}"* ]] \
|
|
|
|
&& die "Duplicate signature: ${sig_filename}";
|
|
|
|
|
|
|
|
[ ! -z "$group_config" ] \
|
|
|
|
&& [[ "${group_config}" != *"${fingerprint}"* ]] \
|
|
|
|
&& die "Signature not in group \"${group}\": ${sig_filename}";
|
|
|
|
|
2020-11-16 11:17:50 +00:00
|
|
|
echo "Verified detached signature by \"${signer}\""
|
2020-11-13 22:40:49 +00:00
|
|
|
|
2020-11-12 22:56:38 +00:00
|
|
|
seen_fingerprints="${seen_fingerprints} ${fingerprint}"
|
|
|
|
((sig_count=sig_count+1))
|
|
|
|
done
|
2020-11-16 11:17:50 +00:00
|
|
|
[[ "$sig_count" -ge "$threshold" ]] || \
|
|
|
|
die "Minimum detached signatures not found: ${sig_count}/${threshold}";
|
2020-11-12 22:56:38 +00:00
|
|
|
}
|
|
|
|
|
2020-11-16 10:40:04 +00:00
|
|
|
### Verify all commits in git repo have valid signatures
|
|
|
|
### Optionally verify a minimum number of valid unique signatures
|
|
|
|
### Optionally verify all signatures belong to keys in gpg alias group
|
|
|
|
verify_git(){
|
|
|
|
[ $# -eq 2 ] || die "Usage: verify_git <threshold> <group>"
|
|
|
|
local threshold="${1}"
|
|
|
|
local group="${2}"
|
2020-11-16 11:17:50 +00:00
|
|
|
local sig_count=0
|
|
|
|
local seen_fingerprints=""
|
|
|
|
local depth=0
|
|
|
|
|
|
|
|
while [[ $depth != "$(git rev-list --count HEAD)" ]]; do
|
|
|
|
ref=HEAD~${depth}
|
|
|
|
commit=$(git log --format="%H" "$ref")
|
|
|
|
fingerprint=$(git log --format="%GP" "$ref" -n1 )
|
|
|
|
signer=$( get_signer "${fingerprint}" )
|
|
|
|
|
|
|
|
git verify-commit HEAD~${depth} >/dev/null 2>&1\
|
|
|
|
|| die "Unsigned commit: ${commit}"
|
|
|
|
|
|
|
|
[[ "${seen_fingerprints}" != *"${fingerprint}"* ]] \
|
|
|
|
&& seen_fingerprints="${seen_fingerprints} ${fingerprint}" \
|
|
|
|
&& ((sig_count=sig_count+1)) \
|
|
|
|
&& echo "Verified git signature at depth ${depth} by \"${signer}\""
|
|
|
|
|
|
|
|
[[ "${sig_count}" -ge "${threshold}" ]] && break;
|
|
|
|
|
|
|
|
((depth=depth+1))
|
|
|
|
done
|
|
|
|
|
|
|
|
[[ "${sig_count}" -ge "${threshold}" ]] \
|
|
|
|
|| die "Minimum git signatures not found: ${sig_count}/${threshold}";
|
2020-11-16 10:40:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
## Public Commands
|
|
|
|
|
2020-11-14 01:15:00 +00:00
|
|
|
cmd_manifest() {
|
|
|
|
mkdir -p ".${PROGRAM}"
|
|
|
|
printf "$(get_files | xargs openssl sha256 -r)" \
|
2020-11-16 10:40:04 +00:00
|
|
|
| sed -e 's/ \*/ /g' -e 's/ \.\// /g' \
|
|
|
|
| LC_ALL=C sort -k2 \
|
|
|
|
> ".${PROGRAM}/manifest.txt"
|
2020-11-14 01:15:00 +00:00
|
|
|
}
|
|
|
|
|
2020-11-12 22:56:38 +00:00
|
|
|
cmd_verify() {
|
2020-11-16 10:40:04 +00:00
|
|
|
local opts min=1 group=""
|
2020-11-13 22:40:49 +00:00
|
|
|
opts="$(getopt -o m:g: -l min:,group: -n "$PROGRAM" -- "$@")"
|
|
|
|
eval set -- "$opts"
|
|
|
|
while true; do case $1 in
|
|
|
|
-m|--min) min="$2"; shift 2 ;;
|
|
|
|
-g|--group) group="$2"; shift 2 ;;
|
|
|
|
--) shift; break ;;
|
|
|
|
esac done
|
|
|
|
|
2020-11-16 10:40:04 +00:00
|
|
|
command -v git >/dev/null 2>&1 \
|
|
|
|
&& ( [ -d .git ] || git rev-parse --git-dir > /dev/null 2>&1 ) \
|
|
|
|
&& verify_git "${min}" "${group}"
|
|
|
|
|
2020-11-13 03:39:11 +00:00
|
|
|
#TODO: if git and if invalid: show diff against last valid version
|
2020-11-13 02:31:26 +00:00
|
|
|
( [ -d ".${PROGRAM}" ] && ls .${PROGRAM}/*.asc >/dev/null 2>&1 ) \
|
|
|
|
|| die "Error: No signatures"
|
2020-11-12 22:56:38 +00:00
|
|
|
cmd_manifest
|
2020-11-13 22:40:49 +00:00
|
|
|
verify_file "${min}" "${group}" .${PROGRAM}/manifest.txt
|
2020-11-12 22:56:38 +00:00
|
|
|
}
|
|
|
|
|
2020-11-13 02:47:11 +00:00
|
|
|
cmd_add(){
|
2020-11-13 02:31:26 +00:00
|
|
|
cmd_manifest
|
|
|
|
gpg --armor --detach-sig .${PROGRAM}/manifest.txt
|
|
|
|
local fingerprint=$( \
|
|
|
|
gpg --list-packets .${PROGRAM}/manifest.txt.asc \
|
|
|
|
| grep "issuer key ID" \
|
|
|
|
| sed 's/.*\([A-Z0-9]\{16\}\).*/\1/g' \
|
|
|
|
)
|
|
|
|
mv .${PROGRAM}/manifest.{"txt.asc","${fingerprint}.asc"}
|
|
|
|
}
|
|
|
|
|
2020-11-12 22:56:38 +00:00
|
|
|
cmd_version() {
|
|
|
|
cat <<-_EOF
|
2020-11-13 22:40:49 +00:00
|
|
|
==========================================
|
|
|
|
= sig: simple multisig trust toolchain =
|
|
|
|
= =
|
|
|
|
= v0.0.1 =
|
|
|
|
= =
|
|
|
|
= https://gitlab.com/pchq/sig =
|
|
|
|
==========================================
|
2020-11-12 22:56:38 +00:00
|
|
|
_EOF
|
|
|
|
}
|
|
|
|
|
|
|
|
cmd_usage() {
|
|
|
|
cmd_version
|
|
|
|
cat <<-_EOF
|
|
|
|
Usage:
|
2020-11-13 22:40:49 +00:00
|
|
|
$PROGRAM verify [--group=<group>,-g <group>] [--min=<N>,-m <N>]
|
2020-11-16 10:40:04 +00:00
|
|
|
Verify m-of-n signatures by given group are present for directory
|
2020-11-13 02:47:11 +00:00
|
|
|
$PROGRAM add
|
2020-11-12 22:56:38 +00:00
|
|
|
Add signature to manifest for this directory
|
2020-11-13 02:31:26 +00:00
|
|
|
$PROGRAM manifest
|
|
|
|
Generate hash manifest for this directory
|
2020-11-12 22:56:38 +00:00
|
|
|
$PROGRAM help
|
|
|
|
Show this text.
|
|
|
|
$PROGRAM version
|
|
|
|
Show version information.
|
|
|
|
_EOF
|
|
|
|
}
|
|
|
|
|
2020-11-16 10:40:04 +00:00
|
|
|
# Verify all tools in this list are installed at needed versions
|
2020-11-14 01:15:00 +00:00
|
|
|
check_tools head cut find sort sed getopt gpg openssl
|
2020-11-13 02:47:11 +00:00
|
|
|
|
2020-11-16 10:40:04 +00:00
|
|
|
# Allow entire script to be namespaced based on filename
|
2020-11-12 22:56:38 +00:00
|
|
|
PROGRAM="${0##*/}"
|
2020-11-16 10:40:04 +00:00
|
|
|
|
|
|
|
# Export public sub-commands
|
2020-11-12 22:56:38 +00:00
|
|
|
case "$1" in
|
2020-11-14 01:15:00 +00:00
|
|
|
verify) shift; cmd_verify "$@" ;;
|
|
|
|
add) shift; cmd_add "$@" ;;
|
|
|
|
manifest) shift; cmd_manifest "$@" ;;
|
|
|
|
version|--version) shift; cmd_version "$@" ;;
|
|
|
|
help|--help) shift; cmd_usage "$@" ;;
|
|
|
|
*) cmd_usage "$@" ;;
|
2020-11-12 22:56:38 +00:00
|
|
|
esac
|