A utility for downloading and verifying FreeBSD releases and snapshots
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

513 lines
12 KiB

  1. #!/bin/sh -
  2. #
  3. # Copyright 2018 John-Mark Gurney.
  4. # All rights reserved.
  5. #
  6. # Redistribution and use in source and binary forms, with or without
  7. # modification, are permitted provided that the following conditions
  8. # are met:
  9. # 1. Redistributions of source code must retain the above copyright
  10. # notice, this list of conditions and the following disclaimer.
  11. # 2. Redistributions in binary form must reproduce the above copyright
  12. # notice, this list of conditions and the following disclaimer in the
  13. # documentation and/or other materials provided with the distribution.
  14. #
  15. # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
  16. # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  17. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  18. # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
  19. # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  20. # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
  21. # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
  22. # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  23. # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
  24. # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  25. # SUCH DAMAGE.
  26. #
  27. # $Id$
  28. #
  29. STOREDIR="$HOME/.snapaid"
  30. KEYS="78B342BA26C7B2AC681EA7BE524F0C37A0B946A3"
  31. KEY_URLS='https://cgit.freebsd.org/doc/plain/documentation/static/pgpkeys/gjb.key'
  32. setdefaults() {
  33. GPG=$(which gpg2)
  34. WGET=$(which wget)
  35. SHASUM=$(which shasum)
  36. }
  37. setdefaults
  38. if [ ! -x "$GPG" ]; then
  39. echo 'Failed to find gpg2 executable'
  40. exit 1
  41. fi
  42. if [ ! -x "$WGET" ]; then
  43. echo 'Failed to find wget executable'
  44. exit 1
  45. fi
  46. if [ ! -x "$SHASUM" ]; then
  47. echo 'Failed to find shasum executable'
  48. exit 1
  49. fi
  50. #wget:
  51. # -N for timestamps
  52. # --backups=x for backing up
  53. hostname=people.FreeBSD.org
  54. hostname=www.funkthat.com
  55. completeurl="https://${hostname}/~jmg/FreeBSD-snap/snapshot.complete.idx.xz"
  56. currenturl="https://${hostname}/~jmg/FreeBSD-snap/snapshot.idx.xz"
  57. # type release arch platform date svnrev xxx fname url mid
  58. # 1 2 3 4 5 6 7 8 9 10
  59. # 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
  60. set -e
  61. # This is used for some testing functions
  62. copy_function() {
  63. declare -F "$1" > /dev/null || return 1
  64. local func="$(declare -f "$1")"
  65. eval "${2}(${func#*\(}"
  66. }
  67. # Test function to cause a bad input
  68. cmd_failure() {
  69. exit 1
  70. }
  71. # First time fails, second time run real command
  72. gpg_first_fails() {
  73. copy_function verifygpg_orig verifygpg
  74. return 1
  75. }
  76. # When first arg is --, just touch the file to fake a bad d/l
  77. bad_file_dl() {
  78. if [ x"$1" = x"--" ]; then
  79. touch $(basename "$2")
  80. else
  81. $WGET_orig "$@"
  82. fi
  83. }
  84. # Make sure that the storage directory is present
  85. mkstore() {
  86. mkdir "$STOREDIR" 2>/dev/null || :
  87. if ! [ -x "$STOREDIR" -a -w "$STOREDIR" -a -d "$STOREDIR" ]; then
  88. echo "$STOREDIR is not a writable directory."
  89. fi
  90. }
  91. check_keys() {
  92. for i in $KEYS; do
  93. if ! $GPG --list-keys "$i" >/dev/null 2>&1; then
  94. return 1
  95. fi
  96. done
  97. return 0
  98. }
  99. # Given a message id, get the raw body and store it.
  100. get_raw() {
  101. mkstore
  102. mid="$1"
  103. midfile="$STOREDIR/$mid".raw
  104. if [ ! -e "$midfile" ]; then
  105. # get the location, it's a database lookup
  106. loc=$($WGET --max-redirect=0 --method=HEAD -S -o - -O - 'https://docs.freebsd.org/cgi/mid.cgi?'"$mid" 2>/dev/null | awk 'tolower($1) == "location:" { print $2; exit }')
  107. if [ x"$loc" = x"" ]; then
  108. # Some emails are sent to both -current and -snapshot,
  109. # such as 20160529215940.GA11785@FreeBSD.org
  110. # try w/ some magic sed
  111. loc=$($WGET -O - 'https://docs.freebsd.org/cgi/mid.cgi?'"$mid" 2>/dev/null |
  112. sed -Ee '/.*(getmsg.cgi?[^"]*).*/!d;s//\/cgi\/\1/' | head -1)
  113. fi
  114. # if it's host relative, add https
  115. if [ x"$loc" != x"${loc#//}" ]; then
  116. # add https
  117. loc="https:$loc"
  118. elif [ x"$loc" != x"${loc#/[^/]}" ]; then
  119. # add https+host
  120. loc="https://docs.freebsd.org$loc"
  121. fi
  122. # get the raw part
  123. tmpfile="$STOREDIR/.tmp.$$.$mid".raw
  124. # strip out everything but message id and first signed part
  125. $WGET -O - "$loc"+raw 2>/dev/null | awk '
  126. tolower($1) == "message-id:" && check == 0 {
  127. print
  128. }
  129. $0 == "-----BEGIN PGP SIGNED MESSAGE-----" {
  130. sigbody = 1
  131. }
  132. sigbody {
  133. print
  134. }
  135. $0 == "-----END PGP SIGNATURE-----" {
  136. sigbody = 0
  137. }' > "$tmpfile"
  138. if verifygpg "$tmpfile"; then
  139. mv "$tmpfile" "$STOREDIR/$mid.raw"
  140. else
  141. rm "$tmpfile"
  142. echo Bad signature from mail archive.
  143. return 1
  144. fi
  145. else
  146. if ! verifygpg "$midfile"; then
  147. rm "$midfile"
  148. get_raw "$mid"
  149. return $?
  150. fi
  151. fi
  152. }
  153. fetch() {
  154. mkstore
  155. if ! (cd "$STOREDIR" && $WGET -N "$1" >/dev/null 2>&1); then
  156. return 1
  157. fi
  158. }
  159. getvermid() {
  160. xzcat "$STOREDIR"/snapshot.complete.idx.xz | awk '$8 == fname {
  161. print $10
  162. }' fname="$i"
  163. }
  164. # takes basename of arg, which much exist in STOREDIR, and verifies
  165. # that the signature is valid.
  166. verifygpg() {
  167. local fname
  168. fname=$(basename "$1")
  169. if ! (cd "$STOREDIR" && $GPG --verify "$fname" 2> /dev/null); then
  170. echo 'ERROR: PGP signature verification failed!'
  171. return 1
  172. fi
  173. }
  174. # Verifies the file
  175. verifyfile() {
  176. local fname
  177. local hashinfo
  178. local algo hash
  179. fname="$STOREDIR/${1}.raw"
  180. hashinfo=$(awk '
  181. check && $2 == "('"$2"')" {
  182. hashes[$1] = $4
  183. }
  184. $0 == "-----BEGIN PGP SIGNED MESSAGE-----" {
  185. check = 1
  186. }
  187. $0 == "-----BEGIN PGP SIGNATURE-----" {
  188. check = 0
  189. }
  190. END {
  191. if ("SHA512" in hashes)
  192. algo = "SHA512"
  193. else if ("SHA256" in hashes)
  194. algo = "SHA256"
  195. else {
  196. print "unkn BADHASH"
  197. exit 1
  198. }
  199. print algo " " hashes[algo]
  200. }
  201. ' "$fname")
  202. read algo hash <<-EOF
  203. ${hashinfo}
  204. EOF
  205. if [ x"$algo" == x"unkn" -o x"$algo" = x"" ]; then
  206. echo 'Unable to find hash for file.'
  207. exit 1
  208. fi
  209. echo "$hash $2" | $SHASUM -a "${algo#SHA}" -c -
  210. }
  211. dlverify() {
  212. fname="$8"
  213. dlurl="$9"
  214. vermid="${10}"
  215. # verify snap email
  216. if ! get_raw "$vermid"; then
  217. echo "Unable to fetch/verify snapshot email for: $fname"
  218. return 1
  219. fi
  220. if ! [ -f $(basename "$dlurl") ]; then
  221. # fetch link
  222. $WGET -- "$dlurl"
  223. else
  224. echo 'Image already exists, verifying...'
  225. fi
  226. if ! verifyfile "$vermid" "$fname"; then
  227. echo 'Removing bad file.'
  228. rm "$fname"
  229. return 1
  230. fi
  231. }
  232. if ! check_keys; then
  233. echo 'Necessary keys have not been imported into key ring.'
  234. echo "Please obtain they following keyid(s):"
  235. echo $KEYS
  236. echo ""
  237. echo "The keys may be obtained from the following URLs:"
  238. for i in $KEY_URLS; do
  239. echo "$i"
  240. done
  241. echo ""
  242. echo "and imported into GPG w/ the --import option. This can be"
  243. echo "done via the command:"
  244. echo "fetch -o - $KEY_URLS | gpg --import -"
  245. echo ""
  246. echo "For extra security, additional verification should be done, such"
  247. echo "as manually verifying finger prints."
  248. exit 3
  249. fi
  250. if [ x"$1" = x"verify" ]; then
  251. shift
  252. if ! fetch "$completeurl"; then
  253. echo Failed to fetch the complete index.
  254. exit 1
  255. fi
  256. for i in "$@"; do
  257. vermid=$(getvermid "$i")
  258. if [ x"$vermid" = x"" ]; then
  259. echo "Unable to find entry for: $i"
  260. continue
  261. fi
  262. if ! get_raw "$vermid"; then
  263. echo "Unable to fetch snapshot email for: $i"
  264. continue
  265. fi
  266. verifyfile "$vermid" "$i"
  267. done
  268. elif [ x"$1" = x"find" ]; then
  269. shift
  270. fetch "$currenturl"
  271. tmpdir=$(mktemp -d -t snapaid)
  272. trap "rm -r $tmpdir" 0
  273. ( cd "$tmpdir";
  274. xzcat "$STOREDIR"/snapshot.idx.xz | sort -r -k 5 -k 2 > selection;
  275. while :; do
  276. cnt=$(wc -l < selection)
  277. if [ x"$1" = x"" ]; then
  278. # display current list
  279. awk '
  280. BEGIN {
  281. # xzcat snapshot.complete.idx.xz | ./maxcol.awk
  282. # xzcat snapshot.complete.idx.xz | awk "{ print $3}" | sort -u
  283. # note that for powerpc-* that first part is dropped
  284. fmtstr = "%2s %-3s %-15s %-14s %-18s %-8s %-11s\n"
  285. printf(fmtstr, "#", "TYP", "RELEASE", "ARCH", "PLATFORM/TYPE", "DATE", "REV")
  286. cnt = 1
  287. }
  288. {
  289. if ($3 ~ /^powerpc-/)
  290. $3 = substr($3, 9)
  291. if ($4 == "xxx")
  292. plt=$7
  293. else
  294. plt=$4
  295. printf(fmtstr, cnt, $1, $2, $3, plt, $5, $6)
  296. if (cnt >= 20)
  297. exit 0
  298. cnt += 1
  299. }
  300. ' selection
  301. fi
  302. if [ x"$1" != x"" ]; then
  303. sel="$1"
  304. shift
  305. else
  306. read -p 'Select image #, enter search term, reset, or quit: ' sel
  307. fi
  308. if [ x"$sel" = x"reset" ]; then
  309. xzcat "$STOREDIR"/snapshot.idx.xz | sort -r -k 5 > selection;
  310. continue
  311. elif [ x"$sel" = x"quit" ]; then
  312. echo "$sel" > sel
  313. break
  314. fi
  315. if [ "$cnt" -gt 20 ]; then
  316. cnt=20
  317. fi
  318. if [ "$sel" -ge 1 -a "$sel" -le "$cnt" ] 2>/dev/null; then
  319. echo $(tail -n +"$sel" selection | head -n 1) > sel
  320. break
  321. else
  322. # restrict
  323. if ! grep -- "$sel" selection > selection.new; then
  324. echo WARNING: Ignoring selection, no results.
  325. else
  326. mv selection.new selection
  327. fi
  328. fi
  329. done
  330. )
  331. sel=$(cat "$tmpdir"/sel)
  332. if [ x"$sel" = x"quit" ]; then
  333. exit 0
  334. fi
  335. set -- ${sel}
  336. echo selected image "$8"
  337. dlverify ${sel}
  338. elif [ x"$1" = x"test" ]; then
  339. # Setup test environment
  340. tmpdir=$(mktemp -d -t snapaid)
  341. trap "rm -r $tmpdir" 0
  342. cd "$tmpdir"
  343. STOREDIR="$tmpdir"/snapaid
  344. # Make sure that the check keys function works.
  345. echo 'Testing check_keys works...'
  346. # Prime the custom keyring
  347. GPG="gpg2 --no-default-keyring --keyring pubring.gpg"
  348. for i in $KEY_URLS; do
  349. $WGET -O - -- "$i" 2>/dev/null | $GPG --import 2>/dev/null
  350. done
  351. if ! check_keys; then
  352. echo failed
  353. exit 1
  354. fi
  355. KEYS_orig="$KEYS"
  356. KEYS="0x1384923867573928" # bogus key
  357. if check_keys; then
  358. echo failed
  359. exit 1
  360. fi
  361. echo passed
  362. # Test a bad download fails
  363. echo 'Testing dlverify...'
  364. WGET_orig="$WGET"
  365. WGET=bad_file_dl
  366. # if dlverify is successsful, then it's a failure
  367. 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
  368. echo 'failed'
  369. exit 1
  370. fi
  371. # Make sure that a bad d/l was not left behind
  372. if [ -e FreeBSD-13.0-CURRENT-sparc64-20181026-r339752-bootonly.iso.xz ]; then
  373. echo failed
  374. exit 1
  375. fi
  376. echo passed
  377. # Test getting the raw file
  378. echo 'Testing get_raw success...'
  379. mid='20160122055622.GA87581@FreeBSD.org'
  380. get_raw "$mid"
  381. # Verify resulsts
  382. (cd "$STOREDIR" && echo '6e53df5995b6cc423c7f2d63b6df52d5d7f70e8586c25f91433fd8a1a2466e77be6a38884bde8bedd9ff6e7deb0215a66e1c2a16e4955503c20445e649a5fb47 20160122055622.GA87581@FreeBSD.org.raw' | $SHASUM -a 512 -c)
  383. echo passed
  384. # If the file already exists, but fails verification, that
  385. # it will refetch and be correct
  386. echo 'Testing get_raw with file already present that fails verification...'
  387. copy_function verifygpg verifygpg_orig
  388. copy_function gpg_first_fails verifygpg
  389. get_raw "$mid"
  390. (cd "$STOREDIR" && echo '6e53df5995b6cc423c7f2d63b6df52d5d7f70e8586c25f91433fd8a1a2466e77be6a38884bde8bedd9ff6e7deb0215a66e1c2a16e4955503c20445e649a5fb47 20160122055622.GA87581@FreeBSD.org.raw' | $SHASUM -a 512 -c)
  391. echo passed
  392. # If the file already exists, a "broken" wget won't cause
  393. # a problem
  394. echo 'Testing get_raw with file already present...'
  395. WGET=cmd_failure
  396. get_raw "$mid"
  397. echo passed
  398. # Test failure
  399. echo 'Testing get_raw fails w/ bad data...'
  400. WGET=cmd_failure
  401. rm "$STOREDIR/$mid.raw"
  402. # it should fail
  403. ! get_raw "$mid"
  404. # and the desired file should not exist
  405. if [ -e "$STOREDIR/$mid.raw" ]; then
  406. echo 'Test failed!'
  407. exit 1;
  408. fi
  409. echo passed
  410. setdefaults
  411. echo tests completed!!!
  412. else
  413. if [ $# -gt 0 ]; then
  414. echo "Unknown verb: $1"
  415. fi
  416. echo "Usage:"
  417. echo " $0 verify file ..."
  418. echo " $0 find [ termselection ... ]"
  419. echo ""
  420. echo "The verify option will attempt to verify each file specified."
  421. echo ""
  422. echo "The find option will start up an interactive session to find"
  423. echo "and select the snapshot to download and verify."
  424. fi