The OpenWRT page about download integrity verification describes three steps for a successful verification of downloaded files:

  1. Download the sha256sum and sha256sum.asc files
  2. Check the signature with gpg --with-fingerprint --verify sha256sum.asc sha256sum, ensure that the GnuPG command reports a good signature and that the fingerprint matches the ones listed on our fingerprints page.
  3. Download the firmware image into the same directory as the sha256sums file and verify its checksum using the following command: sha256sum -c --ignore-missing sha256sums

It has contained a convenience script in the past, but it was removed, due to being unsafe (check the wiki page history). The steps are left for the user to follow and seem straightforward but I believe a little explanation won't hurt. I'll use TP-Link MR200v1 as an example. The aim is to write commands in a way that can be automated using bash:

Download files

A simple wget can serve:

url="https://downloads.openwrt.org/snapshots/targets/ramips/mt7620"
sumsFile="sha256sums"

wget "$url/$sumsFile"
wget "$url/$sumsFile.asc"

Files can be deleted before calling wget, because it does filestamping (adding an incremental number to the end of a filename).

Verify signature

Verifying signature in an automated manner using bash is where I spent most of my time. Simply running the offered command is not sufficient:

gpg --with-fingerprint --verify sha256sums.asc sha256sums

Without any previous work, it results into the following error:

gpg: Can't check signature: No public key

There are numerous ways around the problem, the most automatic is to use --auto-key-retrieve option:

gpg --auto-key-retrieve --with-fingerprint --verify sha256sums.asc

This option assumes, that the keys are published inside a key server. Also note that gpg can assume the right filename if it matches with the signature, so sha256sums can be omitted. The output should look similar to this:

gpg: assuming signed data in 'sha256sums'
gpg: Signature made Sun 04 Apr 2021 03:07:20 CEST
gpg:                using RSA key 6D9278A33A9AB3146262DCECF93525A88B699029
gpg: Good signature from "LEDE Build System (LEDE GnuPG key for unattended build jobs) <lede-adm@lists.infradead.org>" [unknown]
gpg: WARNING: This key is not certified with a trusted signature!
gpg:          There is no indication that the signature belongs to the owner.
Primary key fingerprint: 54CC 7430 7A2C 6DC9 CE61  8269 CD84 BCED 6264 71F1
     Subkey fingerprint: 6D92 78A3 3A9A B314 6262  DCEC F935 25A8 8B69 9029

Visually the focusing on the word Good is the minimal requirement of the the first part second step:

ensure that the GnuPG command reports a good signature

Manually navigating to the fingerprints page and copy-search-paste in a browser for the actual fingerprint, for instance 54CC 7430 7A2C 6DC9 CE61 8269 CD84 BCED 6264 71F1 in this example is required as the second part of the second step.

hat the fingerprint matches the ones listed on our fingerprints page

Test return status

With a focus on automation, the fact that the gpg --verify option returns 0 if the verification was successful and 1 of not (which is a common convention), means testing for the status of the previous command in bash with $? can be used here:

gpg --auto-key-retrieve --with-fingerprint --verify sha256sums.asc

if [ $? -eq 0 ]; then
   echo "SIGNATURE VERIFIED"
else
   echo "SIGNATURE INVALID, the program will terminate"
   exit 1
fi

Fingerprint parsing

Depending on the GnuPG version, the gpg --verify only prints the output to the screen. To parse the output and to do something useful with it in the script, an option --status-fd 1 has to be added as well:

gpg --status-fd 1 --auto-key-retrieve --with-fingerprint --verify sha256sums.asc

The output becomes formatted differently now:

[GNUPG:] NEWSIG
[GNUPG:] KEY_CONSIDERED 54CC74307A2C6DC9CE618269CD84BCED626471F1 0
[GNUPG:] SIG_ID 5XkMbTUScxodW5F+uzE34LmUBk8 2021-04-04 1617498440
[GNUPG:] KEY_CONSIDERED 54CC74307A2C6DC9CE618269CD84BCED626471F1 0
[GNUPG:] GOODSIG F93525A88B699029 LEDE Build System (LEDE GnuPG key for unattended build jobs) <lede-adm@lists.infradead.org>
[GNUPG:] VALIDSIG 6D9278A33A9AB3146262DCECF93525A88B699029 2021-04-04 1617498440 0 4 0 1 10 00 54CC74307A2C6DC9CE618269CD84BCED626471F1
[GNUPG:] KEY_CONSIDERED 54CC74307A2C6DC9CE618269CD84BCED626471F1 0
[GNUPG:] KEY_CONSIDERED 54CC74307A2C6DC9CE618269CD84BCED626471F1 0
[GNUPG:] TRUST_UNDEFINED 0 pgp
[GNUPG:] VERIFICATION_COMPLIANCE_MODE 23

An example grep command extracting the fingerprint of the primary key:

gpgOutput=$(gpg --status-fd 1 --auto-key-retrieve --with-fingerprint --verify sha256sums.asc)
fingerprint=$(gpgOutput | grep -m1 KEY_CONSIDERED | tr -d' ' -f3)
echo $fingerprint

Checking that the fingerprint matches the one published is the most tricky part, because it is only printed on the web page with a custom formatting. Also, it is not foolproof, because should the attackers got hold of the webserver where the fingerprints are published, they could've substituted there different ones. This requires some cognitive effort on the user's part to do the research about what source can be trusted or not, but this is a topic for another day.

Fingerprint formatting

Note that some formatting of the fingerprint usually has to be done here to match the published shape, for instance adding a space every four characters and adding one another space every 25 to match the user-readable output of the gpg --verify can be done like this:

# formats fingerprint 54CC74307A2C6DC9CE618269CD84BCED626471F1
# to prettier form of 54CC 7430 7A2C 6DC9 CE61  8269 CD84 BCED 6264 71F1
formatted=$(echo "$fingerprint" | sed 's/.\{4\}/& /g' | xargs | sed 's/.\{25\}/& /g')

An example checking the presence of a fingerprint on a web page, assuming the fingerprints there are legit:

curl -s "$fingerprintUrl" | grep -o "$formatted"

The grep command also follows the return status convention, so the same test with $? can be applied here, providing an automated solution.

Checksum verification

The third step. Now that the validity of the checksums is verified, they can be used for an actual image file verification, meaning that all the steps before here were just a preparation. But with everything in place, the steps are straightforward:

sumsFile="sha256sums"
url="https://downloads.openwrt.org/snapshots/targets/ramips/mt7620"
imageFile="openwrt-imagebuilder-ramips-mt7620.Linux-x86_64.tar.xz"

wget "$url/$imageFile"
sha256sum -c --ignore-missing "$sumsFile"

The output of the sha256sum tells that the image file (or any other file which sha256 sum is stored in the sha256sums file) was downloaded intact.

openwrt-imagebuilder-ramips-mt7620.Linux-x86_64.tar.xz: OK

The same status test can be employed here as well to automate the script further. Source files performing the steps are also available in the repository.

This is a 25th post of #100daystooffload.