#!/bin/sh - # # Copyright 2018, 2022-2023 John-Mark Gurney. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. # # $Id$ # STOREDIR="$HOME/.snapaid" KEYS="78B342BA26C7B2AC681EA7BE524F0C37A0B946A3 EAF48BBA7CC77A30FEFC0DA938CECA690C6A6A6E" KEY_URLS='https://cgit.freebsd.org/doc/plain/documentation/static/pgpkeys/gjb.key https://cgit.freebsd.org/doc/plain/documentation/static/pgpkeys/cperciva.key' setdefaults() { GPG=$(which gpg2) WGET=$(which wget) SHASUM=$(which shasum) } setdefaults if [ ! -x "$GPG" ]; then echo 'Failed to find gpg2 executable' exit 1 fi if [ ! -x "$WGET" ]; then echo 'Failed to find wget executable' exit 1 fi if [ ! -x "$SHASUM" ]; then echo 'Failed to find shasum executable' exit 1 fi #wget: # -N for timestamps # --backups=x for backing up hostname=people.FreeBSD.org hostname=www.funkthat.com completeurl="https://${hostname}/~jmg/FreeBSD-snap/snapshot.complete.idx.xz" currenturl="https://${hostname}/~jmg/FreeBSD-snap/snapshot.idx.xz" mailarchurl="https://www.funkthat.com/~jmg/FreeBSD-snap/mail/%s" # type release arch platform date svnrev xxx fname url mid # 1 2 3 4 5 6 7 8 9 10 # iso 11.1-STABLE arm-armv6 BEAGLEBONE 20180315 r330998 xxx FreeBSD-11.1-STABLE-arm-armv6-BEAGLEBONE-20180315-r330998.img.xz https://download.freebsd.org/ftp/snapshots/ISO-IMAGES/11.1/FreeBSD-11.1-STABLE-arm-armv6-BEAGLEBONE-20180315-r330998.img.xz 20180316000842.GA7399@FreeBSD.org set -e # This is used for some testing functions copy_function() { declare -F "$1" > /dev/null || return 1 local func="$(declare -f "$1")" eval "${2}(${func#*\(}" } # Test function to cause a bad input cmd_failure() { exit 1 } # First time fails, second time run real command gpg_first_fails() { copy_function verifygpg_orig verifygpg return 1 } # When first arg is --, just touch the file to fake a bad d/l bad_file_dl() { if [ x"$1" = x"--" ]; then touch $(basename "$2") else $WGET_orig "$@" fi } # Make sure that the storage directory is present mkstore() { mkdir "$STOREDIR" 2>/dev/null || : if ! [ -x "$STOREDIR" -a -w "$STOREDIR" -a -d "$STOREDIR" ]; then echo "$STOREDIR is not a writable directory." fi } check_keys() { for i in $KEYS; do if ! $GPG --list-keys "$i" >/dev/null 2>&1; then return 1 fi done return 0 } # Given a message id, get the raw body and store it. get_raw() { mkstore mid="$1" midfile="$STOREDIR/$mid".raw if [ ! -e "$midfile" ]; then # get the raw part tmpfile="$STOREDIR/.tmp.$$.$mid".raw # strip out everything but message id and first signed part # minimizeemail isn't necessary anymore as the archive # already has done it, but keep it $WGET -O - $(printf "$mailarchurl" "$mid") 2>/dev/null | minimizeemail > "$tmpfile" if verifygpg "$tmpfile"; then mv "$tmpfile" "$STOREDIR/$mid.raw" else rm "$tmpfile" echo Bad signature from mail archive. return 1 fi else if ! verifygpg "$midfile"; then rm "$midfile" get_raw "$mid" return $? fi fi } fetch() { mkstore if ! (cd "$STOREDIR" && $WGET -N "$1" >/dev/null 2>&1); then return 1 fi } getvermid() { xzcat "$STOREDIR"/snapshot.complete.idx.xz | awk '$8 == fname { print $10 }' fname="$i" } minimizeemail() { awk ' tolower($1) == "message-id:" && check == 0 { print } tolower($1) == "content-type:" && tolower($2) == "multipart/signed;" && check == 0 { getboundary = 1 print next } getboundary == 1 { getboundary = 0 haveboundary = 1 print if (substr($2, 1, 9) == "boundary=") boundary=substr($2, 11, length($2) - 11) next } haveboundary && $1 == ("--" boundary) { sigbody = 1 } $0 == "-----BEGIN PGP SIGNED MESSAGE-----" { sigbody = 1 } sigbody { print } $0 == "-----END PGP SIGNATURE-----" && !haveboundary { sigbody = 0 }' } verifygpgfile() { local fname fname="$1" if grep "multipart/signed" "$fname" >/dev/null 2>&1; then tmpdir=$(mktemp -d -t snapaid) # Note mkfifo does not work, for some reason I got a # different order, they are small, so just write them to # disk and clean up afterward. awk -v FNAME="$tmpdir"/msg ' BEGIN { if (FNAME == "") { print "FNAME not specified." > "/dev/stderr" exit 1 } } END { #print "exiting" > "/dev/stderr" } { #print "raw " $0 > "/dev/stderr" } $0 ~ "protocol=\"application/pgp-signature" { if (substr($2, 1, 9) == "boundary=") boundary=substr($2, 11, length($2) - 11) #print "boundary " boundary " remaining line: " $0 > "/dev/stderr" next } boundary != "" && $1 == ("--" boundary) { if (state == 0) { output = 1 outfname = FNAME printf("") > outfname state = 1 } else if (state == 1) { close(outfname) outfname = FNAME ".asc" printf("") > outfname state = 2 } else if (state == 2) { close(outfname) output = 0 } #print "state " state ", boundary: " boundary ", output: " output > "/dev/stderr" next } # Do not print the final line ending. It belongs w/ the ending boundary, # and we do not want to prepend it first time through output { printf("%s%s", lineend, $0) >> outfname lineend = "\r\n" }' < "$fname" $GPG --verify "$tmpdir"/msg.asc "$tmpdir"/msg 2>/dev/null exitval="$?" #rm -f "$tmpdir"/msg.asc "$tmpdir"/msg #rmdir "$tmpdir" return "$exitval" else $GPG --verify "$fname" 2>/dev/null fi } # takes basename of arg, which much exist in STOREDIR, and verifies # that the signature is valid. verifygpg() { local fname fname=$(basename "$1") if ! (cd "$STOREDIR" && verifygpgfile "$fname" ); then echo 'ERROR: PGP signature verification failed!' return 1 fi } # Verifies the file verifyfile() { local fname local hashinfo local algo hash fname="$STOREDIR/${1}.raw" hashinfo=$(awk -v FNAME="$2" ' $0 ~ "protocol=\"application/pgp-signature" { if (substr($2, 1, 9) == "boundary=") boundary=substr($2, 11, length($2) - 11) #print "boundary " boundary " remaining line: " $0 > "/dev/stderr" next } boundary != "" && $1 == ("--" boundary) { if (check) check = 0 else check = 1 next } check && $2 == ("(" FNAME ")") { hashes[$1] = $4 } $0 == "-----BEGIN PGP SIGNED MESSAGE-----" { check = 1 } $0 == "-----BEGIN PGP SIGNATURE-----" { check = 0 } END { if ("SHA512" in hashes) algo = "SHA512" else if ("SHA256" in hashes) algo = "SHA256" else { print "unkn BADHASH" exit 1 } print algo " " hashes[algo] } ' "$fname") read algo hash <<-EOF ${hashinfo} EOF if [ x"$algo" == x"unkn" -o x"$algo" = x"" ]; then echo 'Unable to find hash for file.' exit 1 fi echo "$hash $2" | $SHASUM -a "${algo#SHA}" -c - } dlverify() { fname="$8" dlurl="$9" vermid="${10}" # verify snap email if ! get_raw "$vermid"; then echo "Unable to fetch/verify snapshot email for: $fname" return 1 fi if ! [ -f $(basename "$dlurl") ]; then # fetch link $WGET -- "$dlurl" else echo 'Image already exists, verifying...' fi if ! verifyfile "$vermid" "$fname"; then echo 'Removing bad file.' rm "$fname" return 1 fi } # Special wget that doesn't fetch the large file WGET_special1() { if [ "$1" = "--" -a "$2" = "https://download.freebsd.org/ftp/snapshots/ISO-IMAGES/14.0/FreeBSD-14.0-CURRENT-amd64-20210909-58a7bf124cc-249268-bootonly.iso.xz" ]; then return 0 else $WGET_orig "$@" fi } # Allow the file to be sourced so the functions for checking PGP # signatures can be used by addinfo.sh if [ x"$SNAPAID_SH" = x"source" ]; then return 0 fi if ! check_keys; then echo 'Necessary keys have not been imported into key ring.' echo "Please obtain they following keyid(s):" echo $KEYS echo "" echo "The keys may be obtained from the following URLs:" for i in $KEY_URLS; do echo "$i" done echo "" echo "and imported into GPG w/ the --import option. This can be" echo "done via the command:" echo "fetch -o - $KEY_URLS | gpg --import -" echo "" echo "For extra security, additional verification should be done, such" echo "as manually verifying finger prints." exit 3 fi if [ x"$1" = x"verify" ]; then shift if ! fetch "$completeurl"; then echo Failed to fetch the complete index. exit 1 fi for i in "$@"; do vermid=$(getvermid "$i") if [ x"$vermid" = x"" ]; then echo "Unable to find entry for: $i" continue fi if ! get_raw "$vermid"; then echo "Unable to fetch snapshot email for: $i" continue fi verifyfile "$vermid" "$i" done elif [ x"$1" = x"find" ]; then shift fetch "$currenturl" tmpdir=$(mktemp -d -t snapaid) trap "rm -r $tmpdir" 0 ( cd "$tmpdir"; xzcat "$STOREDIR"/snapshot.idx.xz | sort -r -k 5 -k 2 > selection; while :; do cnt=$(wc -l < selection) if [ x"$1" = x"" ]; then # display current list awk ' BEGIN { # xzcat snapshot.complete.idx.xz | ./maxcol.awk # xzcat snapshot.complete.idx.xz | awk "{ print $3}" | sort -u # note that for powerpc-* that first part is dropped fmtstr = "%2s %-3s %-15s %-14s %-18s %-8s %-11s\n" printf(fmtstr, "#", "TYP", "RELEASE", "ARCH", "PLATFORM/TYPE", "DATE", "REV") cnt = 1 } { if ($3 ~ /^powerpc-/) $3 = substr($3, 9) if ($4 == "xxx") plt=$7 else plt=$4 printf(fmtstr, cnt, $1, $2, $3, plt, $5, $6) if (cnt >= 20) exit 0 cnt += 1 } ' selection fi if [ x"$1" != x"" ]; then sel="$1" shift else read -p 'Select image #, enter search term, reset, or quit: ' sel fi if [ x"$sel" = x"reset" ]; then xzcat "$STOREDIR"/snapshot.idx.xz | sort -r -k 5 > selection; continue elif [ x"$sel" = x"quit" ]; then echo "$sel" > sel break fi if [ "$cnt" -gt 20 ]; then cnt=20 fi if [ "$sel" -ge 1 -a "$sel" -le "$cnt" ] 2>/dev/null; then echo $(tail -n +"$sel" selection | head -n 1) > sel break else # restrict if ! grep -- "$sel" selection > selection.new; then echo WARNING: Ignoring selection, no results. else mv selection.new selection fi fi done ) sel=$(cat "$tmpdir"/sel) if [ x"$sel" = x"quit" ]; then exit 0 fi set -- ${sel} echo selected image "$8" dlverify ${sel} elif [ x"$1" = x"test" ]; then # Setup test environment tmpdir=$(mktemp -d -t snapaid) trap "rm -r $tmpdir" 0 cd "$tmpdir" STOREDIR="$tmpdir"/snapaid # Make sure that the check keys function works. echo 'Testing check_keys works...' # Prime the custom keyring GPG="gpg2 --no-default-keyring --keyring pubring.gpg" for i in $KEY_URLS; do $WGET -O - -- "$i" 2>/dev/null | $GPG --import 2>/dev/null done if ! check_keys; then echo failed exit 1 fi KEYS_orig="$KEYS" KEYS="0x1384923867573928" # bogus key if check_keys; then echo failed exit 1 fi echo passed # save WGET & SHASUM WGET_orig="$WGET" SHASUM_orig="$SHASUM" # Testing signature that is an attachment echo 'Testing email with attached signature...' # called by dlverify #get_raw "20210909215942.GH1630@FreeBSD.org" WGET=WGET_special1 SHASUM="echo FreeBSD-14.0-CURRENT-amd64-20210909-58a7bf124cc-249268-bootonly.iso.xz: OK; : " if ! dlverify iso 14.0-CURRENT amd64 xxx 20210909 xxx bootonly FreeBSD-14.0-CURRENT-amd64-20210909-58a7bf124cc-249268-bootonly.iso.xz https://download.freebsd.org/ftp/snapshots/ISO-IMAGES/14.0/FreeBSD-14.0-CURRENT-amd64-20210909-58a7bf124cc-249268-bootonly.iso.xz 20210909215942.GH1630@FreeBSD.org; then echo 'failed' exit 1 fi echo passed WGET="$WGET_orig" SHASUM="$SHASUM_orig" # Test a bad download fails echo 'Testing dlverify...' WGET=bad_file_dl # if dlverify is successsful, then it's a failure if dlverify iso 13.0-CURRENT sparc64 xxx 20181026 r339752 bootonly FreeBSD-13.0-CURRENT-sparc64-20181026-r339752-bootonly.iso.xz https://download.freebsd.org/ftp/snapshots/ISO-IMAGES/13.0/FreeBSD-13.0-CURRENT-sparc64-20181026-r339752-bootonly.iso.xz 20181026184443.GD75350@FreeBSD.org; then echo 'failed' exit 1 fi # Make sure that a bad d/l was not left behind if [ -e FreeBSD-13.0-CURRENT-sparc64-20181026-r339752-bootonly.iso.xz ]; then echo failed exit 1 fi echo passed # Test getting the raw file echo 'Testing get_raw success...' mid='20160122055622.GA87581@FreeBSD.org' get_raw "$mid" # Verify resulsts (cd "$STOREDIR" && echo '6e53df5995b6cc423c7f2d63b6df52d5d7f70e8586c25f91433fd8a1a2466e77be6a38884bde8bedd9ff6e7deb0215a66e1c2a16e4955503c20445e649a5fb47 20160122055622.GA87581@FreeBSD.org.raw' | $SHASUM -a 512 -c) echo passed # If the file already exists, but fails verification, that # it will refetch and be correct echo 'Testing get_raw with file already present that fails verification...' copy_function verifygpg verifygpg_orig copy_function gpg_first_fails verifygpg get_raw "$mid" (cd "$STOREDIR" && echo '6e53df5995b6cc423c7f2d63b6df52d5d7f70e8586c25f91433fd8a1a2466e77be6a38884bde8bedd9ff6e7deb0215a66e1c2a16e4955503c20445e649a5fb47 20160122055622.GA87581@FreeBSD.org.raw' | $SHASUM -a 512 -c) echo passed # If the file already exists, a "broken" wget won't cause # a problem echo 'Testing get_raw with file already present...' WGET=cmd_failure get_raw "$mid" echo passed # Test failure echo 'Testing get_raw fails w/ bad data...' WGET=cmd_failure rm "$STOREDIR/$mid.raw" # it should fail ! get_raw "$mid" # and the desired file should not exist if [ -e "$STOREDIR/$mid.raw" ]; then echo 'Test failed!' exit 1; fi echo passed setdefaults echo tests completed!!! else if [ $# -gt 0 ]; then echo "Unknown verb: $1" fi echo "Usage:" echo " $0 verify file ..." echo " $0 find [ termselection ... ]" echo "" echo "The verify option will attempt to verify each file specified." echo "" echo "The find option will start up an interactive session to find" echo "and select the snapshot to download and verify." fi