set -e
unset GREP_OPTIONS
-GPG_CMD="gpg --ignore-time-conflict --no-options --no-default-keyring"
-
-# gpg needs a trustdb to function, but it can't be invalid (not even empty)
-# so we create a temporary directory to store our fresh readable trustdb in
-TRUSTDBDIR="$(mktemp -d)"
-CURRENTTRAP="${CURRENTTRAP} rm -rf '${TRUSTDBDIR}';"
-trap "${CURRENTTRAP}" 0 HUP INT QUIT ILL ABRT FPE SEGV PIPE TERM
-chmod 700 "$TRUSTDBDIR"
-# We also don't use a secret keyring, of course, but gpg panics and
-# implodes if there isn't one available - and writeable for imports
-SECRETKEYRING="${TRUSTDBDIR}/secring.gpg"
-touch $SECRETKEYRING
-GPG_CMD="$GPG_CMD --secret-keyring $SECRETKEYRING"
-GPG_CMD="$GPG_CMD --trustdb-name ${TRUSTDBDIR}/trustdb.gpg"
-
-# now create the trustdb with an (empty) dummy keyring
-$GPG_CMD --quiet --check-trustdb --keyring $SECRETKEYRING
-# and make sure that gpg isn't trying to update the file
-GPG_CMD="$GPG_CMD --no-auto-check-trustdb --trust-model always"
-
-GPG="$GPG_CMD"
-
APT_DIR="/"
eval $(apt-config shell APT_DIR Dir)
eval $(apt-config shell REMOVED_KEYS APT::Key::RemovedKeys)
ARCHIVE_KEYRING_URI='&keyring-uri;'
eval $(apt-config shell ARCHIVE_KEYRING_URI APT::Key::ArchiveKeyringURI)
-TMP_KEYRING=${APT_DIR}/var/lib/apt/keyrings/maybe-import-keyring.gpg
+
+aptkey_echo() { echo "$@"; }
requires_root() {
if [ "$(id -u)" -ne 0 ]; then
- echo >&1 "ERROR: This command can only be used by root."
+ echo >&2 "ERROR: This command can only be used by root."
exit 1
fi
}
-# gpg defaults to mode 0600 for new keyrings. Create one with 0644 instead.
-init_keyring() {
- for path; do
- if ! [ -e "$path" ]; then
- touch -- "$path"
- chmod 0644 -- "$path"
- fi
+get_fingerprints_of_keyring() {
+ $GPG_CMD --keyring "$1" --with-colons --fingerprint | while read publine; do
+ # search for a public key
+ if [ "${publine%%:*}" != 'pub' ]; then continue; fi
+ # search for the associated fingerprint (should be the very next line)
+ while read fprline; do
+ if [ "${fprline%%:*}" = 'sub' ]; then break; # should never happen
+ elif [ "${fprline%%:*}" != 'fpr' ]; then continue; fi
+ echo "$fprline" | cut -d':' -f 10
+ done
done
}
MASTER=$2
if [ ! -f "$ADD_KEYRING" ]; then
- echo "ERROR: '$ADD_KEYRING' not found"
+ echo >&2 "ERROR: '$ADD_KEYRING' not found"
return
- fi
+ fi
if [ ! -f "$MASTER" ]; then
- echo "ERROR: '$MASTER' not found"
+ echo >&2 "ERROR: '$MASTER' not found"
return
fi
# is honored. so:
# all keys that are exported must have a valid signature
# from a key in the $distro-master-keyring
- add_keys=`$GPG_CMD --keyring $ADD_KEYRING --with-colons --list-keys | grep ^pub | cut -d: -f5`
+ add_keys="$(get_fingerprints_of_keyring "$ADD_KEYRING")"
all_add_keys=`$GPG_CMD --keyring $ADD_KEYRING --with-colons --list-keys | grep ^[ps]ub | cut -d: -f5`
master_keys=`$GPG_CMD --keyring $MASTER --with-colons --list-keys | grep ^pub | cut -d: -f5`
fi
done
done
-
+
for add_key in $add_keys; do
# export the add keyring one-by-one
- rm -f $TMP_KEYRING
- $GPG_CMD --keyring $ADD_KEYRING --output $TMP_KEYRING --export $add_key
- # check if signed with the master key and only add in this case
- ADDED=0
+ local TMP_KEYRING="${GPGHOMEDIR}/tmp-keyring.gpg"
+ $GPG_CMD --batch --yes --keyring "$ADD_KEYRING" --output "$TMP_KEYRING" --export "$add_key"
+ if ! $GPG_CMD --batch --yes --keyring "$TMP_KEYRING" --import "$MASTER" > "${GPGHOMEDIR}/gpgoutput.log" 2>&1; then
+ cat "${GPGHOMEDIR}/gpgoutput.log"
+ false
+ fi
+ # check if signed with the master key and only add in this case
+ ADDED=0
for master_key in $master_keys; do
- if $GPG_CMD --keyring $MASTER --keyring $TMP_KEYRING --check-sigs --with-colons $add_key | grep '^sig:!:' | cut -d: -f5 | grep -q $master_key; then
- $GPG --import $TMP_KEYRING
+ if $GPG_CMD --keyring $TMP_KEYRING --check-sigs --with-colons $add_key | grep '^sig:!:' | cut -d: -f5 | grep -q $master_key; then
+ $GPG_CMD --batch --yes --keyring "$ADD_KEYRING" --export "$add_key" | $GPG --batch --yes --import
ADDED=1
fi
done
if [ $ADDED = 0 ]; then
echo >&2 "Key '$add_key' not added. It is not signed with a master key"
fi
+ rm -f "${TMP_KEYRING}"
done
- rm -f $TMP_KEYRING
}
# update the current archive signing keyring from a network URI
echo >&2 "ERROR: Your distribution is not supported in net-update as no uri for the archive-keyring is set"
exit 1
fi
- requires_root
# in theory we would need to depend on wget for this, but this feature
# isn't useable in debian anyway as we have no keyring uri nor a master key
if ! which wget >/dev/null 2>&1; then
if [ ! -d ${APT_DIR}/var/lib/apt/keyrings ]; then
mkdir -p ${APT_DIR}/var/lib/apt/keyrings
fi
- keyring=${APT_DIR}/var/lib/apt/keyrings/$(basename $ARCHIVE_KEYRING)
+ keyring=${APT_DIR}/var/lib/apt/keyrings/$(basename $ARCHIVE_KEYRING_URI)
old_mtime=0
if [ -e $keyring ]; then
old_mtime=$(stat -c %Y $keyring)
fi
new_mtime=$(stat -c %Y $keyring)
if [ $new_mtime -ne $old_mtime ]; then
- echo "Checking for new archive signing keys now"
+ aptkey_echo "Checking for new archive signing keys now"
add_keys_with_verify_against_master_keyring $keyring $MASTER_KEYRING
fi
}
echo >&2 "Is the &keyring-package; package installed?"
exit 1
fi
- requires_root
# add new keys from the package;
if [ -r "$REMOVED_KEYS" ]; then
# remove no-longer supported/used keys
- keys=`$GPG_CMD --keyring $REMOVED_KEYS --with-colons --list-keys | grep ^pub | cut -d: -f5`
- for key in $keys; do
- if $GPG --list-keys --with-colons | grep ^pub | cut -d: -f5 | grep -q $key; then
- $GPG --quiet --batch --delete-key --yes ${key}
- fi
+ get_fingerprints_of_keyring "$REMOVED_KEYS" | while read key; do
+ foreach_keyring_do 'remove_key_from_keyring' "$key"
done
else
- echo "Warning: removed keys keyring $REMOVED_KEYS missing or not readable" >&2
+ echo >&2 "Warning: removed keys keyring $REMOVED_KEYS missing or not readable"
fi
}
remove_key_from_keyring() {
- local GPG="$GPG_CMD --keyring $1"
- # check if the key is in this keyring: the key id is in the 5 column at the end
- if ! $GPG --with-colons --list-keys 2>&1 | grep -q "^pub:[^:]*:[^:]*:[^:]*:[0-9A-F]\+$2:"; then
- return
- fi
- if [ ! -w "$1" ]; then
- echo >&2 "Key ${2} is in keyring ${1}, but can't be removed as it is read only."
- return
+ local KEYRINGFILE="$1"
+ shift
+ # non-existent keyrings have by definition no keys
+ if [ ! -e "$KEYRINGFILE" ]; then
+ return
fi
- # check if it is the only key in the keyring and if so remove the keyring alltogether
- if [ '1' = "$($GPG --with-colons --list-keys | grep "^pub:[^:]*:[^:]*:[^:]*:[0-9A-F]\+:" | wc -l)" ]; then
- mv -f "$1" "${1}~" # behave like gpg
- return
- fi
- # we can't just modify pointed to files as these might be in /usr or something
- local REALTARGET
- if [ -L "$1" ]; then
- REALTARGET="$(readlink -f "$1")"
- mv -f "$1" "${1}.dpkg-tmp"
- cp -a "$REALTARGET" "$1"
- ls "$(dirname $1)"
- fi
- # delete the key from the keyring
- $GPG --batch --delete-key --yes "$2"
- if [ -n "$REALTARGET" ]; then
- # the real backup is the old link, not the copy we made
- mv -f "${1}.dpkg-tmp" "${1}~"
- fi
-}
-remove_key() {
- requires_root
+ local GPG="$GPG_CMD --keyring $KEYRINGFILE"
+ for KEY in "$@"; do
+ # check if the key is in this keyring: the key id is in the 5 column at the end
+ if ! get_fingerprints_of_keyring "$KEYRINGFILE" | grep -q "^[0-9A-F]*${KEY}$"; then
+ continue
+ fi
+ if [ ! -w "$KEYRINGFILE" ]; then
+ echo >&2 "Key ${KEY} is in keyring ${KEYRINGFILE}, but can't be removed as it is read only."
+ continue
+ fi
+ # check if it is the only key in the keyring and if so remove the keyring altogether
+ if [ '1' = "$(get_fingerprints_of_keyring "$KEYRINGFILE" | wc -l)" ]; then
+ mv -f "$KEYRINGFILE" "${KEYRINGFILE}~" # behave like gpg
+ return
+ fi
+ # we can't just modify pointed to files as these might be in /usr or something
+ local REALTARGET
+ if [ -L "$KEYRINGFILE" ]; then
+ REALTARGET="$(readlink -f "$KEYRINGFILE")"
+ mv -f "$KEYRINGFILE" "${KEYRINGFILE}.dpkg-tmp"
+ cp -a "$REALTARGET" "$KEYRINGFILE"
+ fi
+ # delete the key from the keyring
+ $GPG --batch --delete-key --yes "$KEY"
+ if [ -n "$REALTARGET" ]; then
+ # the real backup is the old link, not the copy we made
+ mv -f "${KEYRINGFILE}.dpkg-tmp" "${KEYRINGFILE}~"
+ fi
+ done
+}
- # if a --keyring was given, just remove from there
- if [ -n "$FORCED_KEYRING" ]; then
- remove_key_from_keyring "$FORCED_KEYRING" "$1"
- else
+foreach_keyring_do() {
+ local ACTION="$1"
+ shift
+ # if a --keyring was given, just remove from there
+ if [ -n "$FORCED_KEYRING" ]; then
+ $ACTION "$FORCED_KEYRING" "$@"
+ else
# otherwise all known keyrings are up for inspection
- local TRUSTEDFILE="/etc/apt/trusted.gpg"
- eval $(apt-config shell TRUSTEDFILE Apt::GPGV::TrustedKeyring)
- eval $(apt-config shell TRUSTEDFILE Dir::Etc::Trusted/f)
- remove_key_from_keyring "$TRUSTEDFILE" "$1"
- TRUSTEDPARTS="/etc/apt/trusted.gpg.d"
+ if [ -s "$TRUSTEDFILE" ]; then
+ $ACTION "$TRUSTEDFILE" "$@"
+ fi
+ local TRUSTEDPARTS="/etc/apt/trusted.gpg.d"
eval $(apt-config shell TRUSTEDPARTS Dir::Etc::TrustedParts/d)
if [ -d "$TRUSTEDPARTS" ]; then
+ # strip / suffix as gpg will double-slash in that case (#665411)
+ local STRIPPED_TRUSTEDPARTS="${TRUSTEDPARTS%/}"
+ if [ "${STRIPPED_TRUSTEDPARTS}/" = "$TRUSTEDPARTS" ]; then
+ TRUSTEDPARTS="$STRIPPED_TRUSTEDPARTS"
+ fi
for trusted in $(run-parts --list "$TRUSTEDPARTS" --regex '^.*\.gpg$'); do
- remove_key_from_keyring "$trusted" "$1"
+ if [ -s "$trusted" ]; then
+ $ACTION "$trusted" "$@"
+ fi
done
fi
+ fi
+}
+
+run_cmd_on_keyring() {
+ local KEYRINGFILE="$1"
+ shift
+ # fingerprint and co will fail if key isn't in this keyring
+ $GPG_CMD --keyring "$KEYRINGFILE" --batch "$@" 2>/dev/null || true
+}
+
+import_keys_from_keyring() {
+ local IMPORT="$1"
+ local KEYRINGFILE="$2"
+ if ! $GPG_CMD --keyring "$KEYRINGFILE" --batch --import "$IMPORT" > "${GPGHOMEDIR}/gpgoutput.log" 2>&1; then
+ cat "${GPGHOMEDIR}/gpgoutput.log"
+ false
fi
- echo "OK"
}
+merge_keys_into_keyrings() {
+ local KEYRINGFILE="$1"
+ local IMPORT="$2"
+ if ! $GPG_CMD --keyring "$KEYRINGFILE" --batch --import --import-options 'merge-only' "$IMPORT" > "${GPGHOMEDIR}/gpgoutput.log" 2>&1; then
+ cat "${GPGHOMEDIR}/gpgoutput.log"
+ false
+ fi
+}
+
+merge_back_changes() {
+ if [ -n "$FORCED_KEYRING" ]; then
+ # if the keyring was forced merge is already done
+ return
+ fi
+ if [ -s "${GPGHOMEDIR}/pubring.gpg" ]; then
+ # merge all updated keys
+ foreach_keyring_do 'merge_keys_into_keyrings' "${GPGHOMEDIR}/pubring.gpg"
+ fi
+ # look for keys which were added or removed
+ get_fingerprints_of_keyring "${GPGHOMEDIR}/pubring.orig.gpg" > "${GPGHOMEDIR}/pubring.orig.keylst"
+ get_fingerprints_of_keyring "${GPGHOMEDIR}/pubring.gpg" > "${GPGHOMEDIR}/pubring.keylst"
+ sort "${GPGHOMEDIR}/pubring.keylst" "${GPGHOMEDIR}/pubring.orig.keylst" | uniq --unique | while read key; do
+ if grep -q "^${key}$" "${GPGHOMEDIR}/pubring.orig.keylst"; then
+ # key isn't part of new keyring, so remove
+ foreach_keyring_do 'remove_key_from_keyring' "$key"
+ elif grep -q "^${key}$" "${GPGHOMEDIR}/pubring.keylst"; then
+ # key is part of new keyring, so we need to import it
+ create_new_keyring "$TRUSTEDFILE"
+ if ! $GPG --batch --yes --export "$key" | $GPG_CMD --keyring "$TRUSTEDFILE" --batch --yes --import > "${GPGHOMEDIR}/gpgoutput.log" 2>&1; then
+ cat "${GPGHOMEDIR}/gpgoutput.log"
+ false
+ fi
+ else
+ echo >&2 "Errror: Key ${key} (dis)appeared out of nowhere"
+ fi
+ done
+}
+
+setup_merged_keyring() {
+ if [ -z "$FORCED_KEYRING" ]; then
+ foreach_keyring_do 'import_keys_from_keyring' "${GPGHOMEDIR}/pubring.gpg"
+ if [ -r "${GPGHOMEDIR}/pubring.gpg" ]; then
+ cp -a "${GPGHOMEDIR}/pubring.gpg" "${GPGHOMEDIR}/pubring.orig.gpg"
+ else
+ touch "${GPGHOMEDIR}/pubring.gpg" "${GPGHOMEDIR}/pubring.orig.gpg"
+ fi
+ GPG="$GPG --keyring ${GPGHOMEDIR}/pubring.gpg"
+ else
+ GPG="$GPG --keyring $TRUSTEDFILE"
+ create_new_keyring "$TRUSTEDFILE"
+ fi
+}
+
+create_new_keyring() {
+ # gpg defaults to mode 0600 for new keyrings. Create one with 0644 instead.
+ if ! [ -e "$TRUSTEDFILE" ]; then
+ if [ -w "$(dirname "$TRUSTEDFILE")" ]; then
+ touch -- "$TRUSTEDFILE"
+ chmod 0644 -- "$TRUSTEDFILE"
+ fi
+ fi
+}
usage() {
echo "Usage: apt-key [--keyring file] [command] [arguments]"
shift
TRUSTEDFILE="$1"
FORCED_KEYRING="$1"
- if [ -r "$TRUSTEDFILE" ] || [ "$2" = 'add' ] || [ "$2" = 'adv' ]; then
- GPG="$GPG --keyring $TRUSTEDFILE --primary-keyring $TRUSTEDFILE"
- else
- echo >&2 "Error: The specified keyring »$TRUSTEDFILE« is missing or not readable"
- exit 1
- fi
+ ;;
+ --secret-keyring)
shift
+ FORCED_SECRET_KEYRING="$1"
+ ;;
+ --readonly)
+ merge_back_changes() { true; }
;;
--fakeroot)
requires_root() { true; }
- shift
+ ;;
+ --quiet)
+ aptkey_echo() { true; }
;;
--*)
echo >&2 "Unknown option: $1"
*)
break;;
esac
+ shift
done
if [ -z "$TRUSTEDFILE" ]; then
TRUSTEDFILE="/etc/apt/trusted.gpg"
eval $(apt-config shell TRUSTEDFILE Apt::GPGV::TrustedKeyring)
eval $(apt-config shell TRUSTEDFILE Dir::Etc::Trusted/f)
- if [ -r "$TRUSTEDFILE" ]; then
- GPG="$GPG --keyring $TRUSTEDFILE"
- fi
- GPG="$GPG --primary-keyring $TRUSTEDFILE"
- TRUSTEDPARTS="/etc/apt/trusted.gpg.d"
- eval $(apt-config shell TRUSTEDPARTS Dir::Etc::TrustedParts/d)
- if [ -d "$TRUSTEDPARTS" ]; then
- # strip / suffix as gpg will double-slash in that case (#665411)
- STRIPPED_TRUSTEDPARTS="${TRUSTEDPARTS%/}"
- if [ "${STRIPPED_TRUSTEDPARTS}/" = "$TRUSTEDPARTS" ]; then
- TRUSTEDPARTS="$STRIPPED_TRUSTEDPARTS"
- fi
- for trusted in $(run-parts --list "$TRUSTEDPARTS" --regex '^.*\.gpg$'); do
- GPG="$GPG --keyring $trusted"
- done
- fi
fi
command="$1"
fi
shift
-if [ "$command" != "help" ] && ! which gpg >/dev/null 2>&1; then
- echo >&2 "Warning: gnupg does not seem to be installed."
- echo >&2 "Warning: apt-key requires gnupg for most operations."
- echo >&2
+if [ "$command" != "help" ]; then
+ eval $(apt-config shell GPG_EXE Apt::Key::gpgcommand)
+
+ if [ -n "$GPG_EXE" ] && which "$GPG_EXE" >/dev/null 2>&1; then
+ true
+ elif which gpg >/dev/null 2>&1; then
+ GPG_EXE="gpg"
+ elif which gpg2 >/dev/null 2>&1; then
+ GPG_EXE="gpg2"
+ else
+ echo >&2 "Error: gnupg or gnupg2 do not seem to be installed,"
+ echo >&2 "Error: but apt-key requires gnupg or gnupg2 for operation."
+ echo >&2
+ exit 255
+ fi
+
+ GPG_CMD="$GPG_EXE --ignore-time-conflict --no-options --no-default-keyring"
+
+ # gpg needs (in different versions more or less) files to function correctly,
+ # so we give it its own homedir and generate some valid content for it
+ GPGHOMEDIR="$(mktemp -d)"
+ CURRENTTRAP="${CURRENTTRAP} rm -rf '${GPGHOMEDIR}';"
+ trap "${CURRENTTRAP}" 0 HUP INT QUIT ILL ABRT FPE SEGV PIPE TERM
+ chmod 700 "$GPGHOMEDIR"
+ # We don't use a secret keyring, of course, but gpg panics and
+ # implodes if there isn't one available - and writeable for imports
+ SECRETKEYRING="${GPGHOMEDIR}/secring.gpg"
+ touch $SECRETKEYRING
+ GPG_CMD="$GPG_CMD --homedir $GPGHOMEDIR"
+ # create the trustdb with an (empty) dummy keyring
+ # older gpgs required it, newer gpgs even warn that it isn't needed,
+ # but require it nonetheless for some commands, so we just play safe
+ # here for the foreseeable future and create a dummy one
+ $GPG_CMD --quiet --check-trustdb --keyring $SECRETKEYRING >/dev/null 2>&1
+ # tell gpg that it shouldn't try to maintain a trustdb file
+ GPG_CMD="$GPG_CMD --no-auto-check-trustdb --trust-model always"
+ GPG="$GPG_CMD"
+
+ # for advanced operations, we might really need a secret keyring after all
+ if [ -n "$FORCED_SECRET_KEYRING" ] && [ -r "$FORCED_SECRET_KEYRING" ]; then
+ rm -f "$SECRETKEYRING"
+ cp -a "$FORCED_SECRET_KEYRING" "$SECRETKEYRING"
+ fi
fi
case "$command" in
add)
- requires_root
- init_keyring "$TRUSTEDFILE"
- $GPG --quiet --batch --import "$1"
- echo "OK"
+ requires_root
+ setup_merged_keyring
+ $GPG --quiet --batch --import "$@"
+ merge_back_changes
+ aptkey_echo "OK"
;;
del|rm|remove)
- init_keyring "$TRUSTEDFILE"
- remove_key "$1"
+ requires_root
+ foreach_keyring_do 'remove_key_from_keyring' "$@"
+ aptkey_echo "OK"
;;
update)
- init_keyring "$TRUSTEDFILE"
+ requires_root
+ setup_merged_keyring
update
+ merge_back_changes
;;
net-update)
- init_keyring "$TRUSTEDFILE"
+ requires_root
+ setup_merged_keyring
net_update
+ merge_back_changes
;;
list)
- init_keyring "$TRUSTEDFILE"
- $GPG --batch --list-keys
- ;;
+ foreach_keyring_do 'run_cmd_on_keyring' --list-keys "$@"
+ ;;
finger*)
- init_keyring "$TRUSTEDFILE"
- $GPG --batch --fingerprint
- ;;
- export)
- init_keyring "$TRUSTEDFILE"
- $GPG --armor --export "$1"
- ;;
- exportall)
- init_keyring "$TRUSTEDFILE"
- $GPG --armor --export
- ;;
+ foreach_keyring_do 'run_cmd_on_keyring' --fingerprint "$@"
+ ;;
+ export|exportall)
+ foreach_keyring_do 'import_keys_from_keyring' "${GPGHOMEDIR}/pubring.gpg"
+ $GPG_CMD --keyring "${GPGHOMEDIR}/pubring.gpg" --armor --export "$@"
+ ;;
adv*)
- init_keyring "$TRUSTEDFILE"
- echo "Executing: $GPG $*"
- $GPG $*
- ;;
+ setup_merged_keyring
+ aptkey_echo "Executing: $GPG $*"
+ $GPG "$@"
+ merge_back_changes
+ ;;
+ verify)
+ setup_merged_keyring
+ if which gpgv >/dev/null 2>&1; then
+ gpgv --homedir "${GPGHOMEDIR}" --keyring "${GPGHOMEDIR}/pubring.gpg" --ignore-time-conflict "$@"
+ else
+ $GPG --verify "$@"
+ fi
+ ;;
help)
usage
;;