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.
 
 
 
 

491 lines
11 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="0x524F0C37A0B946A3"
  31. KEY_URLS='https://svnweb.freebsd.org/doc/head/share/pgpkeys/gjb.key?view=co'
  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. completeurl="https://${hostname}/~jmg/FreeBSD-snap/snapshot.complete.idx.xz"
  55. currenturl="https://${hostname}/~jmg/FreeBSD-snap/snapshot.idx.xz"
  56. # type release arch platform date svnrev xxx fname url mid
  57. # 1 2 3 4 5 6 7 8 9 10
  58. # 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
  59. set -e
  60. # This is used for some testing functions
  61. copy_function() {
  62. declare -F "$1" > /dev/null || return 1
  63. local func="$(declare -f "$1")"
  64. eval "${2}(${func#*\(}"
  65. }
  66. # Test function to cause a bad input
  67. cmd_failure() {
  68. exit 1
  69. }
  70. # First time fails, second time run real command
  71. gpg_first_fails() {
  72. copy_function verifygpg_orig verifygpg
  73. return 1
  74. }
  75. # When first arg is --, just touch the file to fake a bad d/l
  76. bad_file_dl() {
  77. if [ x"$1" = x"--" ]; then
  78. touch $(basename "$2")
  79. else
  80. $WGET_orig "$@"
  81. fi
  82. }
  83. # Make sure that the storage directory is present
  84. mkstore() {
  85. mkdir "$STOREDIR" 2>/dev/null || :
  86. if ! [ -x "$STOREDIR" -a -w "$STOREDIR" -a -d "$STOREDIR" ]; then
  87. echo "$STOREDIR is not a writable directory."
  88. fi
  89. }
  90. check_keys() {
  91. for i in $KEYS; do
  92. if ! $GPG --list-keys "$i" >/dev/null 2>&1; then
  93. return 1
  94. fi
  95. done
  96. return 0
  97. }
  98. # Given a message id, get the raw body and store it.
  99. get_raw() {
  100. mkstore
  101. mid="$1"
  102. midfile="$STOREDIR/$mid".raw
  103. if [ ! -e "$midfile" ]; then
  104. # get the location, it's a database lookup
  105. 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 }')
  106. # Some emails are sent to both -current and -snapshot,
  107. # we can't handle them for now
  108. # such as 20160529215940.GA11785@FreeBSD.org
  109. if [ x"$loc" = x"" ]; then
  110. return 1
  111. fi
  112. # if it's relative, add https
  113. if [ x"$loc" != x"${loc#//}" ]; then
  114. # add https
  115. loc="https:$loc"
  116. fi
  117. # get the raw part
  118. tmpfile="$STOREDIR/.tmp.$$.$mid".raw
  119. # strip out everything but message id and first signed part
  120. $WGET -O - "$loc"+raw 2>/dev/null | awk '
  121. tolower($1) == "message-id:" && check == 0 {
  122. print
  123. }
  124. $0 == "-----BEGIN PGP SIGNED MESSAGE-----" {
  125. sigbody = 1
  126. }
  127. sigbody {
  128. print
  129. }
  130. $0 == "-----END PGP SIGNATURE-----" {
  131. sigbody = 0
  132. }' > "$tmpfile"
  133. if verifygpg "$tmpfile"; then
  134. mv "$tmpfile" "$STOREDIR/$mid.raw"
  135. else
  136. rm "$tmpfile"
  137. echo Bad signature from mail archive.
  138. return 1
  139. fi
  140. else
  141. if ! verifygpg "$midfile"; then
  142. rm "$midfile"
  143. get_raw "$mid"
  144. return $?
  145. fi
  146. fi
  147. }
  148. fetch() {
  149. mkstore
  150. if ! (cd "$STOREDIR" && $WGET -N "$1" >/dev/null 2>&1); then
  151. return 1
  152. fi
  153. }
  154. getvermid() {
  155. xzcat "$STOREDIR"/snapshot.complete.idx.xz | awk '$8 == fname {
  156. print $10
  157. }' fname="$i"
  158. }
  159. # takes basename of arg, which much exist in STOREDIR, and verifies
  160. # that the signature is valid.
  161. verifygpg() {
  162. local fname
  163. fname=$(basename "$1")
  164. if ! (cd "$STOREDIR" && $GPG --verify "$fname" 2> /dev/null); then
  165. echo 'ERROR: PGP signature verification failed!'
  166. return 1
  167. fi
  168. }
  169. # Verifies the file
  170. verifyfile() {
  171. local fname
  172. local hashinfo
  173. local algo hash
  174. fname="$STOREDIR/${1}.raw"
  175. hashinfo=$(awk '
  176. check && $2 == "('"$2"')" {
  177. hashes[$1] = $4
  178. }
  179. $0 == "-----BEGIN PGP SIGNED MESSAGE-----" {
  180. check = 1
  181. }
  182. $0 == "-----BEGIN PGP SIGNATURE-----" {
  183. check = 0
  184. }
  185. END {
  186. if ("SHA512" in hashes)
  187. algo = "SHA512"
  188. else if ("SHA256" in hashes)
  189. algo = "SHA256"
  190. else {
  191. print "unkn BADHASH"
  192. exit 1
  193. }
  194. print algo " " hashes[algo]
  195. }
  196. ' "$fname")
  197. read algo hash <<-EOF
  198. ${hashinfo}
  199. EOF
  200. if [ x"$algo" == x"unkn" -o x"$algo" = x"" ]; then
  201. echo 'Unable to find hash for file.'
  202. exit 1
  203. fi
  204. echo "$hash $2" | $SHASUM -a "${algo#SHA}" -c -
  205. }
  206. dlverify() {
  207. fname="$8"
  208. dlurl="$9"
  209. vermid="${10}"
  210. # verify snap email
  211. if ! get_raw "$vermid"; then
  212. echo "Unable to fetch/verify snapshot email for: $fname"
  213. return 1
  214. fi
  215. if ! [ -f $(basename "$dlurl") ]; then
  216. # fetch link
  217. $WGET -- "$dlurl"
  218. else
  219. echo 'Image already exists, verifying...'
  220. fi
  221. if ! verifyfile "$vermid" "$fname"; then
  222. echo 'Removing bad file.'
  223. rm "$fname"
  224. return 1
  225. fi
  226. }
  227. if ! check_keys; then
  228. echo 'Necessary keys have not been imported into key ring.'
  229. echo "Please obtain they following keyid(s):"
  230. echo $KEYS
  231. echo ""
  232. echo "The keys may be obtained from the following URLs:"
  233. for i in $KEY_URLS; do
  234. echo "$i"
  235. done
  236. echo ""
  237. echo "and imported into GPG w/ the --import option."
  238. echo ""
  239. echo "For extra security, additional verification should be done, such"
  240. echo "as manually verifying finger prints."
  241. exit 3
  242. fi
  243. if [ x"$1" = x"verify" ]; then
  244. shift
  245. if ! fetch "$completeurl"; then
  246. echo Failed to fetch the complete index.
  247. exit 1
  248. fi
  249. for i in "$@"; do
  250. vermid=$(getvermid "$i")
  251. if [ x"$vermid" = x"" ]; then
  252. echo "Unable to find entry for: $i"
  253. continue
  254. fi
  255. if ! get_raw "$vermid"; then
  256. echo "Unable to fetch snapshot email for: $i"
  257. continue
  258. fi
  259. verifyfile "$vermid" "$i"
  260. done
  261. elif [ x"$1" = x"find" ]; then
  262. fetch "$currenturl"
  263. tmpdir=$(mktemp -d -t snapaid)
  264. trap "rm -r $tmpdir" 0
  265. ( cd "$tmpdir";
  266. xzcat "$STOREDIR"/snapshot.idx.xz | sort -r -k 5 > selection;
  267. while :; do
  268. # display current list
  269. cnt=$(wc -l < selection)
  270. awk '
  271. BEGIN {
  272. fmtstr = "%2s %-3s %-15s %-18s %-18s %-8s %-7s\n"
  273. printf(fmtstr, "#", "TYP", "RELEASE", "ARCH", "PLATFORM/TYPE", "DATE", "SVNREV")
  274. cnt = 1
  275. }
  276. {
  277. if ($4 == "xxx")
  278. plt=$7
  279. else
  280. plt=$4
  281. printf(fmtstr, cnt, $1, $2, $3, plt, $5, $6)
  282. if (cnt >= 20)
  283. exit 0
  284. cnt += 1
  285. }
  286. ' selection
  287. read -p 'Select image #, enter search term, reset, or quit: ' sel
  288. if [ x"$sel" = x"reset" ]; then
  289. xzcat "$STOREDIR"/snapshot.idx.xz | sort -r -k 5 > selection;
  290. continue
  291. elif [ x"$sel" = x"quit" ]; then
  292. echo "$sel" > sel
  293. break
  294. fi
  295. if [ "$cnt" -gt 20 ]; then
  296. cnt=20
  297. fi
  298. if [ "$sel" -ge 1 -a "$sel" -le "$cnt" ] 2>/dev/null; then
  299. echo $(tail -n +"$sel" selection | head -n 1) > sel
  300. break
  301. else
  302. # restrict
  303. if ! grep -- "$sel" selection > selection.new; then
  304. echo WARNING: Ignoring selection, no results.
  305. else
  306. mv selection.new selection
  307. fi
  308. fi
  309. done
  310. )
  311. sel=$(cat "$tmpdir"/sel)
  312. if [ x"$sel" = x"quit" ]; then
  313. exit 0
  314. fi
  315. set -- ${sel}
  316. echo selected image "$8"
  317. dlverify ${sel}
  318. elif [ x"$1" = x"test" ]; then
  319. # Setup test environment
  320. tmpdir=$(mktemp -d -t snapaid)
  321. trap "rm -r $tmpdir" 0
  322. cd "$tmpdir"
  323. STOREDIR="$tmpdir"/snapaid
  324. # Make sure that the check keys function works.
  325. echo 'Testing check_keys works...'
  326. # Prime the custom keyring
  327. GPG="gpg2 --no-default-keyring --keyring pubring.gpg"
  328. for i in $KEY_URLS; do
  329. $WGET -O - -- "$i" 2>/dev/null | $GPG --import 2>/dev/null
  330. done
  331. if ! check_keys; then
  332. echo failed
  333. exit 1
  334. fi
  335. KEYS_orig="$KEYS"
  336. KEYS="0x1384923867573928" # bogus key
  337. if check_keys; then
  338. echo failed
  339. exit 1
  340. fi
  341. echo passed
  342. # Test a bad download fails
  343. echo 'Testing dlverify...'
  344. WGET_orig="$WGET"
  345. WGET=bad_file_dl
  346. # if dlverify is successsful, then it's a failure
  347. 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
  348. echo 'failed'
  349. exit 1
  350. fi
  351. # Make sure that a bad d/l was not left behind
  352. if [ -e FreeBSD-13.0-CURRENT-sparc64-20181026-r339752-bootonly.iso.xz ]; then
  353. echo failed
  354. exit 1
  355. fi
  356. echo passed
  357. # Test getting the raw file
  358. echo 'Testing get_raw success...'
  359. mid='20160122055622.GA87581@FreeBSD.org'
  360. get_raw "$mid"
  361. # Verify resulsts
  362. (cd "$STOREDIR" && echo '6e53df5995b6cc423c7f2d63b6df52d5d7f70e8586c25f91433fd8a1a2466e77be6a38884bde8bedd9ff6e7deb0215a66e1c2a16e4955503c20445e649a5fb47 20160122055622.GA87581@FreeBSD.org.raw' | $SHASUM -a 512 -c)
  363. echo passed
  364. # If the file already exists, but fails verification, that
  365. # it will refetch and be correct
  366. echo 'Testing get_raw with file already present that fails verification...'
  367. copy_function verifygpg verifygpg_orig
  368. copy_function gpg_first_fails verifygpg
  369. get_raw "$mid"
  370. (cd "$STOREDIR" && echo '6e53df5995b6cc423c7f2d63b6df52d5d7f70e8586c25f91433fd8a1a2466e77be6a38884bde8bedd9ff6e7deb0215a66e1c2a16e4955503c20445e649a5fb47 20160122055622.GA87581@FreeBSD.org.raw' | $SHASUM -a 512 -c)
  371. echo passed
  372. # If the file already exists, a "broken" wget won't cause
  373. # a problem
  374. echo 'Testing get_raw with file already present...'
  375. WGET=cmd_failure
  376. get_raw "$mid"
  377. echo passed
  378. # Test failure
  379. echo 'Testing get_raw fails w/ bad data...'
  380. WGET=cmd_failure
  381. rm "$STOREDIR/$mid.raw"
  382. # it should fail
  383. ! get_raw "$mid"
  384. # and the desired file should not exist
  385. if [ -e "$STOREDIR/$mid.raw" ]; then
  386. echo 'Test failed!'
  387. exit 1;
  388. fi
  389. echo passed
  390. setdefaults
  391. echo tests completed!!!
  392. else
  393. echo "Unknown verb: $1"
  394. echo "Usage:"
  395. echo " $0 verify file ..."
  396. echo " $0 find"
  397. echo ""
  398. echo "The verify option will attempt to verify each file specified."
  399. echo ""
  400. echo "The find option will start up an interactive session to find"
  401. echo "and select the snapshot to download and verify."
  402. fi