Download key with `gpg --recv-key` and simultaneously check fingerprint in a script Download key with `gpg --recv-key` and simultaneously check fingerprint in a script docker docker

Download key with `gpg --recv-key` and simultaneously check fingerprint in a script


Solution:

#!/bin/bashset -etempName=$(mktemp)gpg --status-fd 1 --keyserver keyserver.ubuntu.com --recv-keys $fingerprint 1> $tempName 2>/dev/nullgrep "^\[GNUPG\:\] IMPORT_OK "[[:digit:]]*" "$fingerprint"$" $tempNamegrep "^\[GNUPG\:\] IMPORT_RES 1" $tempName

This script will return with a non-zero exit code if the key can't be downloaded or if the keyserver returns a malicious key. Beware: The malicious key still ends up in the gpg keyring, so you if you use it outside of a Dockerfile you will probably want to restore the original key ring afterwards. The commands for use in a Dockerfile (adding a rust PPA as an example):

RUN echo "deb http://ppa.launchpad.net/hansjorg/rust/ubuntu trusty main" >> /etc/apt/sources.listRUN echo "deb-src http://ppa.launchpad.net/hansjorg/rust/ubuntu trusty main" >> /etc/apt/sources.listRUN bash -c 'set -e;tempName=$(mktemp);apt-key adv --status-fd 1 --keyserver keyserver.ubuntu.com --recv-keys C03264CD6CADC10BFD6E708B37FD5E80BD6B6386 1> $tempName 2>/dev/null;grep "^\[GNUPG\:\] IMPORT_OK [[:digit:]]* C03264CD6CADC10BFD6E708B37FD5E80BD6B6386$" $tempName;grep "^\[GNUPG\:\] IMPORT_RES 1" $tempName'

Explanation:

The first building block to consider is GnuPGs --status-fd option. It tells gpg to write machine readable output to the given file descriptor. The file descriptor 1 always references stdout, so we will use that. Then we will have to find out what the output of --status-fd looks like. The documentation for that is not in the manpage, but in doc/DETAILS. An example output looks like this:

# gpg --status-fd 1 --keyserver keyserver.ubuntu.com --recv-keys BD6B6386 2>/dev/null[GNUPG:] IMPORTED 37FD5E80BD6B6386 Launchpad PPA for Hans Jørgen Hoel[GNUPG:] IMPORT_OK 1 C03264CD6CADC10BFD6E708B37FD5E80BD6B6386[GNUPG:] IMPORT_RES 1 0 1 1 0 0 0 0 0 0 0 0 0 0

So we are looking for IMPORT_OK and IMPORT_RES lines. The second parameter after IMPORT_OK is the actual fingerprint of the imported key. The first parameter after IMPORT_RES is the number of keys imported.

In the output, gpg escapes newlines, so it is ok to match for lines beginning with [GNUPG:] to assert we not matching in strings that are controlled by an attacker (for example the name field in the key could otherwise contain \n[GNUPG:] IMPORT_OK 1 C03264CD6CADC10BFD6E708B37FD5E80BD6B6386] and fool us by creating a match).

With grep we can match for a line beginning with [GNUPG] sometext via grep "^\[GNUPG\:\]" and for a whole line with grep "^\[GNUPG\:\] sometext$" (^ and $ represent the start and end of a line). According to the documentation, any number following IMPORT_OK is ok for us, so we match against "[[:digit:]]*". Thus, as regular expressions we get "^\[GNUPG\:\] IMPORT_OK "[[:digit:]]*" "$fingerprint"$" and "^\[GNUPG\:\] IMPORT_RES 1"

As we want to match the output twice, we safe it into a temporary file (by creating an empty temporary file with mktemp and rerouting the output in that file). If grep does not match any lines it returns with a non-zero error code. We can use that by instructing bash to abort on any error via set -e. Overall we end up with:

set -etempName=$(mktemp)gpg --status-fd 1 --keyserver keyserver.ubuntu.com --recv-keys $fingerprint 1> $tempName 2>/dev/nullgrep "^\[GNUPG\:\] IMPORT_OK "[[:digit:]]*" "$fingerprint"$" $tempNamegrep "^\[GNUPG\:\] IMPORT_RES 1" $tempName

How to use with apt to add a repository key:apt-key features the adv command to hand over command line parameters directly to gpg (run the above command without output rerouting to see the actual gpg command generated by apt-key). So we can simply exchange gpg with apt-key adv to operate on the repository key ring.


Actually I've come across this page searching for solution to problem similar to OP described. Mentioned gpg --recv-key $fingerprint solution is OK and should be supported by gpg in most common distributions. But in my case I had another limitation. Gpg's network communication features was moved to separate package dirmngr some time ago. The package is not included in default Ubuntu installation starting from Yakkety Yak (i.e. this bug) and you have to install it manually to make above command work. I was trying to avoid that as long as my Docker image is very minimal Ubuntu setup. So I have found some sort of alternative solution which might be useful. Namely I am installing Nginx server from repositories placed at nginx.org this way:

RUN set -Eeuxo pipefail; \    # The keyring is placed in temporary directory    export GNUPGHOME="$(mktemp -d)"; \    # Nginx public key (used for signing packages and repositories)    NGINX_GPGKEY=0x573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62; \    # Pool of high-available keyservers    KEYSERVER=ha.pool.sks-keyservers.net:11371; \    # HKP protocol can be easily represented as HTTP query. The key is imported into temporary keyring.    curl -LfSs "http://$KEYSERVER/pks/lookup?op=get&search=$NGINX_GPGKEY&options=mr&exact=on" | gpg --import -; \    # Additional check that imported key is the right one before copying it to apt trusted database    gpg --export "$NGINX_GPGKEY" | apt-key --keyring /etc/apt/trusted.gpg.d/nginx.gpg add -; \    # Adding nginx.org repository to the sources list    echo "deb [arch=amd64] http://nginx.org/packages/mainline/ubuntu/ $(. /etc/lsb-release; echo $DISTRIB_CODENAME) nginx" > /etc/apt/sources.list.d/nginx.list; \    # Installing Nginx    apt-get update; \    apt-get install -y --no-install-recommends --no-install-suggests nginx; \    # Removing apt cached files    apt-get clean; \    rm -rf /var/lib/apt/lists/*; \    # Removing temporary keyring    rm -rf "$GNUPGHOME"

The code is excerpt from Dockerfile, to run it in shell remove RUN part and all comments (lines starting with #). Also if not run inside Dockerfile exporting GNUPGHOME could interfere with normal gpg behavior. To avoid that either run the whole command in sub-shell - wrap with parenthesis (command) or run unset afterwards - unset GNUPGHOME. So the key is retrieved from keyserver with curl (plain http query and specific URL parameters are used to emulate HKP protocol) and imported to temporary keyring. Then by exporting it with specific KEYID parameter we proofing the key identity in case the keyserver's answer was tampered. The key is imported to distinct apt keyring /etc/apt/trusted.gpg.d/nginx.gpg to be able easily remove it later by deleting the file. Actually there is no need to use apt-key, you could instead redirect gpg --export output to file: gpg --export "$NGINX_GPGKEY" > /etc/apt/trusted.gpg.d/nginx.gpg

Commands are quite self-explanatory, just some additional remarks: HKP protocol description can be found here, also there is good explanation why use the initial set.