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.
 
 
 
 

460 lines
9.6 KiB

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