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
.