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.
 
 
 
 

662 lines
15 KiB

  1. #!/bin/sh -
  2. #
  3. # Copyright 2018, 2022 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 EAF48BBA7CC77A30FEFC0DA938CECA690C6A6A6E"
  31. KEY_URLS='https://cgit.freebsd.org/doc/plain/documentation/static/pgpkeys/gjb.key https://cgit.freebsd.org/doc/plain/documentation/static/pgpkeys/cperciva.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 | minimizeemail > "$tmpfile"
  126. if verifygpg "$tmpfile"; then
  127. mv "$tmpfile" "$STOREDIR/$mid.raw"
  128. else
  129. rm "$tmpfile"
  130. echo Bad signature from mail archive.
  131. return 1
  132. fi
  133. else
  134. if ! verifygpg "$midfile"; then
  135. rm "$midfile"
  136. get_raw "$mid"
  137. return $?
  138. fi
  139. fi
  140. }
  141. fetch() {
  142. mkstore
  143. if ! (cd "$STOREDIR" && $WGET -N "$1" >/dev/null 2>&1); then
  144. return 1
  145. fi
  146. }
  147. getvermid() {
  148. xzcat "$STOREDIR"/snapshot.complete.idx.xz | awk '$8 == fname {
  149. print $10
  150. }' fname="$i"
  151. }
  152. minimizeemail() {
  153. awk '
  154. tolower($1) == "message-id:" && check == 0 {
  155. print
  156. }
  157. tolower($1) == "content-type:" && tolower($2) == "multipart/signed;" && check == 0 {
  158. getboundary = 1
  159. print
  160. next
  161. }
  162. getboundary == 1 {
  163. getboundary = 0
  164. haveboundary = 1
  165. print
  166. if (substr($2, 1, 9) == "boundary=")
  167. boundary=substr($2, 11, length($2) - 11)
  168. next
  169. }
  170. haveboundary && $1 == ("--" boundary) {
  171. sigbody = 1
  172. }
  173. $0 == "-----BEGIN PGP SIGNED MESSAGE-----" {
  174. sigbody = 1
  175. }
  176. sigbody {
  177. print
  178. }
  179. $0 == "-----END PGP SIGNATURE-----" && !haveboundary {
  180. sigbody = 0
  181. }'
  182. }
  183. verifygpgfile() {
  184. local fname
  185. fname="$1"
  186. if grep "multipart/signed" "$fname" >/dev/null 2>&1; then
  187. tmpdir=$(mktemp -d -t snapaid)
  188. # Note mkfifo does not work, for some reason I got a
  189. # different order, they are small, so just write them to
  190. # disk and clean up afterward.
  191. awk -v FNAME="$tmpdir"/msg '
  192. BEGIN {
  193. if (FNAME == "") {
  194. print "FNAME not specified." > "/dev/stderr"
  195. exit 1
  196. }
  197. }
  198. END {
  199. #print "exiting" > "/dev/stderr"
  200. }
  201. {
  202. #print "raw " $0 > "/dev/stderr"
  203. }
  204. $0 ~ "protocol=\"application/pgp-signature" {
  205. if (substr($2, 1, 9) == "boundary=")
  206. boundary=substr($2, 11, length($2) - 11)
  207. #print "boundary " boundary " remaining line: " $0 > "/dev/stderr"
  208. next
  209. }
  210. boundary != "" && $1 == ("--" boundary) {
  211. if (state == 0) {
  212. output = 1
  213. outfname = FNAME
  214. printf("") > outfname
  215. state = 1
  216. } else if (state == 1) {
  217. close(outfname)
  218. outfname = FNAME ".asc"
  219. printf("") > outfname
  220. state = 2
  221. } else if (state == 2) {
  222. close(outfname)
  223. output = 0
  224. }
  225. #print "state " state ", boundary: " boundary ", output: " output > "/dev/stderr"
  226. next
  227. }
  228. # Do not print the final line ending. It belongs w/ the ending boundary,
  229. # and we do not want to prepend it first time through
  230. output {
  231. printf("%s%s", lineend, $0) >> outfname
  232. lineend = "\r\n"
  233. }' < "$fname"
  234. $GPG --verify "$tmpdir"/msg.asc "$tmpdir"/msg 2>/dev/null
  235. exitval="$?"
  236. #rm -f "$tmpdir"/msg.asc "$tmpdir"/msg
  237. #rmdir "$tmpdir"
  238. return "$exitval"
  239. else
  240. $GPG --verify "$fname" 2>/dev/null
  241. fi
  242. }
  243. # takes basename of arg, which much exist in STOREDIR, and verifies
  244. # that the signature is valid.
  245. verifygpg() {
  246. local fname
  247. fname=$(basename "$1")
  248. if ! (cd "$STOREDIR" && verifygpgfile "$fname" ); then
  249. echo 'ERROR: PGP signature verification failed!'
  250. return 1
  251. fi
  252. }
  253. # Verifies the file
  254. verifyfile() {
  255. local fname
  256. local hashinfo
  257. local algo hash
  258. fname="$STOREDIR/${1}.raw"
  259. hashinfo=$(awk -v FNAME="$2" '
  260. $0 ~ "protocol=\"application/pgp-signature" {
  261. if (substr($2, 1, 9) == "boundary=")
  262. boundary=substr($2, 11, length($2) - 11)
  263. #print "boundary " boundary " remaining line: " $0 > "/dev/stderr"
  264. next
  265. }
  266. boundary != "" && $1 == ("--" boundary) {
  267. if (check)
  268. check = 0
  269. else
  270. check = 1
  271. next
  272. }
  273. check && $2 == ("(" FNAME ")") {
  274. hashes[$1] = $4
  275. }
  276. $0 == "-----BEGIN PGP SIGNED MESSAGE-----" {
  277. check = 1
  278. }
  279. $0 == "-----BEGIN PGP SIGNATURE-----" {
  280. check = 0
  281. }
  282. END {
  283. if ("SHA512" in hashes)
  284. algo = "SHA512"
  285. else if ("SHA256" in hashes)
  286. algo = "SHA256"
  287. else {
  288. print "unkn BADHASH"
  289. exit 1
  290. }
  291. print algo " " hashes[algo]
  292. }
  293. ' "$fname")
  294. read algo hash <<-EOF
  295. ${hashinfo}
  296. EOF
  297. if [ x"$algo" == x"unkn" -o x"$algo" = x"" ]; then
  298. echo 'Unable to find hash for file.'
  299. exit 1
  300. fi
  301. echo "$hash $2" | $SHASUM -a "${algo#SHA}" -c -
  302. }
  303. dlverify() {
  304. fname="$8"
  305. dlurl="$9"
  306. vermid="${10}"
  307. # verify snap email
  308. if ! get_raw "$vermid"; then
  309. echo "Unable to fetch/verify snapshot email for: $fname"
  310. return 1
  311. fi
  312. if ! [ -f $(basename "$dlurl") ]; then
  313. # fetch link
  314. $WGET -- "$dlurl"
  315. else
  316. echo 'Image already exists, verifying...'
  317. fi
  318. if ! verifyfile "$vermid" "$fname"; then
  319. echo 'Removing bad file.'
  320. rm "$fname"
  321. return 1
  322. fi
  323. }
  324. # Special wget that doesn't fetch the large file
  325. WGET_special1() {
  326. 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
  327. return 0
  328. else
  329. $WGET_orig "$@"
  330. fi
  331. }
  332. # Allow the file to be sourced so the functions for checking PGP
  333. # signatures can be used by addinfo.sh
  334. if [ x"$SNAPAID_SH" = x"source" ]; then
  335. return 0
  336. fi
  337. if ! check_keys; then
  338. echo 'Necessary keys have not been imported into key ring.'
  339. echo "Please obtain they following keyid(s):"
  340. echo $KEYS
  341. echo ""
  342. echo "The keys may be obtained from the following URLs:"
  343. for i in $KEY_URLS; do
  344. echo "$i"
  345. done
  346. echo ""
  347. echo "and imported into GPG w/ the --import option. This can be"
  348. echo "done via the command:"
  349. echo "fetch -o - $KEY_URLS | gpg --import -"
  350. echo ""
  351. echo "For extra security, additional verification should be done, such"
  352. echo "as manually verifying finger prints."
  353. exit 3
  354. fi
  355. if [ x"$1" = x"verify" ]; then
  356. shift
  357. if ! fetch "$completeurl"; then
  358. echo Failed to fetch the complete index.
  359. exit 1
  360. fi
  361. for i in "$@"; do
  362. vermid=$(getvermid "$i")
  363. if [ x"$vermid" = x"" ]; then
  364. echo "Unable to find entry for: $i"
  365. continue
  366. fi
  367. if ! get_raw "$vermid"; then
  368. echo "Unable to fetch snapshot email for: $i"
  369. continue
  370. fi
  371. verifyfile "$vermid" "$i"
  372. done
  373. elif [ x"$1" = x"find" ]; then
  374. shift
  375. fetch "$currenturl"
  376. tmpdir=$(mktemp -d -t snapaid)
  377. trap "rm -r $tmpdir" 0
  378. ( cd "$tmpdir";
  379. xzcat "$STOREDIR"/snapshot.idx.xz | sort -r -k 5 -k 2 > selection;
  380. while :; do
  381. cnt=$(wc -l < selection)
  382. if [ x"$1" = x"" ]; then
  383. # display current list
  384. awk '
  385. BEGIN {
  386. # xzcat snapshot.complete.idx.xz | ./maxcol.awk
  387. # xzcat snapshot.complete.idx.xz | awk "{ print $3}" | sort -u
  388. # note that for powerpc-* that first part is dropped
  389. fmtstr = "%2s %-3s %-15s %-14s %-18s %-8s %-11s\n"
  390. printf(fmtstr, "#", "TYP", "RELEASE", "ARCH", "PLATFORM/TYPE", "DATE", "REV")
  391. cnt = 1
  392. }
  393. {
  394. if ($3 ~ /^powerpc-/)
  395. $3 = substr($3, 9)
  396. if ($4 == "xxx")
  397. plt=$7
  398. else
  399. plt=$4
  400. printf(fmtstr, cnt, $1, $2, $3, plt, $5, $6)
  401. if (cnt >= 20)
  402. exit 0
  403. cnt += 1
  404. }
  405. ' selection
  406. fi
  407. if [ x"$1" != x"" ]; then
  408. sel="$1"
  409. shift
  410. else
  411. read -p 'Select image #, enter search term, reset, or quit: ' sel
  412. fi
  413. if [ x"$sel" = x"reset" ]; then
  414. xzcat "$STOREDIR"/snapshot.idx.xz | sort -r -k 5 > selection;
  415. continue
  416. elif [ x"$sel" = x"quit" ]; then
  417. echo "$sel" > sel
  418. break
  419. fi
  420. if [ "$cnt" -gt 20 ]; then
  421. cnt=20
  422. fi
  423. if [ "$sel" -ge 1 -a "$sel" -le "$cnt" ] 2>/dev/null; then
  424. echo $(tail -n +"$sel" selection | head -n 1) > sel
  425. break
  426. else
  427. # restrict
  428. if ! grep -- "$sel" selection > selection.new; then
  429. echo WARNING: Ignoring selection, no results.
  430. else
  431. mv selection.new selection
  432. fi
  433. fi
  434. done
  435. )
  436. sel=$(cat "$tmpdir"/sel)
  437. if [ x"$sel" = x"quit" ]; then
  438. exit 0
  439. fi
  440. set -- ${sel}
  441. echo selected image "$8"
  442. dlverify ${sel}
  443. elif [ x"$1" = x"test" ]; then
  444. # Setup test environment
  445. tmpdir=$(mktemp -d -t snapaid)
  446. trap "rm -r $tmpdir" 0
  447. cd "$tmpdir"
  448. STOREDIR="$tmpdir"/snapaid
  449. # Make sure that the check keys function works.
  450. echo 'Testing check_keys works...'
  451. # Prime the custom keyring
  452. GPG="gpg2 --no-default-keyring --keyring pubring.gpg"
  453. for i in $KEY_URLS; do
  454. $WGET -O - -- "$i" 2>/dev/null | $GPG --import 2>/dev/null
  455. done
  456. if ! check_keys; then
  457. echo failed
  458. exit 1
  459. fi
  460. KEYS_orig="$KEYS"
  461. KEYS="0x1384923867573928" # bogus key
  462. if check_keys; then
  463. echo failed
  464. exit 1
  465. fi
  466. echo passed
  467. # save WGET & SHASUM
  468. WGET_orig="$WGET"
  469. SHASUM_orig="$SHASUM"
  470. # Testing signature that is an attachment
  471. echo 'Testing email with attached signature...'
  472. # called by dlverify
  473. #get_raw "20210909215942.GH1630@FreeBSD.org"
  474. WGET=WGET_special1
  475. SHASUM="echo FreeBSD-14.0-CURRENT-amd64-20210909-58a7bf124cc-249268-bootonly.iso.xz: OK; : "
  476. 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
  477. echo 'failed'
  478. exit 1
  479. fi
  480. echo passed
  481. WGET="$WGET_orig"
  482. SHASUM="$SHASUM_orig"
  483. # Test a bad download fails
  484. echo 'Testing dlverify...'
  485. WGET=bad_file_dl
  486. # if dlverify is successsful, then it's a failure
  487. 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
  488. echo 'failed'
  489. exit 1
  490. fi
  491. # Make sure that a bad d/l was not left behind
  492. if [ -e FreeBSD-13.0-CURRENT-sparc64-20181026-r339752-bootonly.iso.xz ]; then
  493. echo failed
  494. exit 1
  495. fi
  496. echo passed
  497. # Test getting the raw file
  498. echo 'Testing get_raw success...'
  499. mid='20160122055622.GA87581@FreeBSD.org'
  500. get_raw "$mid"
  501. # Verify resulsts
  502. (cd "$STOREDIR" && echo '6e53df5995b6cc423c7f2d63b6df52d5d7f70e8586c25f91433fd8a1a2466e77be6a38884bde8bedd9ff6e7deb0215a66e1c2a16e4955503c20445e649a5fb47 20160122055622.GA87581@FreeBSD.org.raw' | $SHASUM -a 512 -c)
  503. echo passed
  504. # If the file already exists, but fails verification, that
  505. # it will refetch and be correct
  506. echo 'Testing get_raw with file already present that fails verification...'
  507. copy_function verifygpg verifygpg_orig
  508. copy_function gpg_first_fails verifygpg
  509. get_raw "$mid"
  510. (cd "$STOREDIR" && echo '6e53df5995b6cc423c7f2d63b6df52d5d7f70e8586c25f91433fd8a1a2466e77be6a38884bde8bedd9ff6e7deb0215a66e1c2a16e4955503c20445e649a5fb47 20160122055622.GA87581@FreeBSD.org.raw' | $SHASUM -a 512 -c)
  511. echo passed
  512. # If the file already exists, a "broken" wget won't cause
  513. # a problem
  514. echo 'Testing get_raw with file already present...'
  515. WGET=cmd_failure
  516. get_raw "$mid"
  517. echo passed
  518. # Test failure
  519. echo 'Testing get_raw fails w/ bad data...'
  520. WGET=cmd_failure
  521. rm "$STOREDIR/$mid.raw"
  522. # it should fail
  523. ! get_raw "$mid"
  524. # and the desired file should not exist
  525. if [ -e "$STOREDIR/$mid.raw" ]; then
  526. echo 'Test failed!'
  527. exit 1;
  528. fi
  529. echo passed
  530. setdefaults
  531. echo tests completed!!!
  532. else
  533. if [ $# -gt 0 ]; then
  534. echo "Unknown verb: $1"
  535. fi
  536. echo "Usage:"
  537. echo " $0 verify file ..."
  538. echo " $0 find [ termselection ... ]"
  539. echo ""
  540. echo "The verify option will attempt to verify each file specified."
  541. echo ""
  542. echo "The find option will start up an interactive session to find"
  543. echo "and select the snapshot to download and verify."
  544. fi