]> git.saurik.com Git - apt.git/blobdiff - cmdline/apt-key.in
add apt-key support for armored GPG key files (*.asc)
[apt.git] / cmdline / apt-key.in
index 49056f2a6e813d07bd2caa54a8b6c694aca5b4c1..c9ff4b3f44c4277bcdb0c247d71021510c905fb1 100644 (file)
@@ -17,7 +17,7 @@ aptkey_echo() { echo "$@"; }
 
 requires_root() {
        if [ "$(id -u)" -ne 0 ]; then
-               echo >&2 "ERROR: This command can only be used by root."
+               apt_error "This command can only be used by root."
                exit 1
        fi
 }
@@ -61,11 +61,11 @@ add_keys_with_verify_against_master_keyring() {
     MASTER="$2"
 
     if [ ! -f "$ADD_KEYRING" ]; then
-       echo >&2 "ERROR: '$ADD_KEYRING' not found"
+       apt_error "Keyring '$ADD_KEYRING' to be added not found"
        return
     fi
     if [ ! -f "$MASTER" ]; then
-       echo >&2 "ERROR: '$MASTER' not found"
+       apt_error "Master-Keyring '$MASTER' not found"
        return
     fi
 
@@ -127,13 +127,13 @@ net_update() {
     fi
 
     if [ -z "$ARCHIVE_KEYRING_URI" ]; then
-       echo >&2 "ERROR: Your distribution is not supported in net-update as no uri for the archive-keyring is set"
+       apt_error 'Your distribution is not supported in net-update as no uri for the archive-keyring is set'
        exit 1
     fi
     # 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 ! command_available 'wget'; then
-       echo >&2 "ERROR: an installed wget is required for a network-based update"
+       apt_error 'wget is required for a network-based update, but it is not installed'
        exit 1
     fi
     if [ ! -d "${APT_DIR}/var/lib/apt/keyrings" ]; then
@@ -156,9 +156,15 @@ net_update() {
 }
 
 update() {
+    if [ -z "$APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE" ]; then
+       echo >&2 "Warning: 'apt-key update' is deprecated and should not be used anymore!"
+       if [ -z "$ARCHIVE_KEYRING" ]; then
+           echo >&2 "Note: In your distribution this command is a no-op and can therefore be removed safely."
+           exit 0
+       fi
+    fi
     if [ ! -f "$ARCHIVE_KEYRING" ]; then
-       echo >&2 "ERROR: Can't find the archive-keyring"
-       echo >&2 "Is the &keyring-package; package installed?"
+       apt_error "Can't find the archive-keyring (Is the &keyring-package; package installed?)"
        exit 1
     fi
 
@@ -173,11 +179,11 @@ update() {
 
     if [ -r "$REMOVED_KEYS" ]; then
        # remove no-longer supported/used keys
-       get_fingerprints_of_keyring "$REMOVED_KEYS" | while read key; do
+       get_fingerprints_of_keyring "$(dearmor_filename "$REMOVED_KEYS")" | while read key; do
            foreach_keyring_do 'remove_key_from_keyring' "$key"
        done
     else
-       echo >&2 "Warning: removed keys keyring  $REMOVED_KEYS missing or not readable"
+       apt_warn "Removed keys keyring '$REMOVED_KEYS' missing or not readable"
     fi
 }
 
@@ -189,19 +195,20 @@ remove_key_from_keyring() {
        return
     fi
 
-    for KEY in "$@"; do
-       local FINGERPRINTS="${GPGHOMEDIR}/keyringfile.keylst"
-       get_fingerprints_of_keyring "$KEYRINGFILE" > "$FINGERPRINTS"
+    local FINGERPRINTS="${GPGHOMEDIR}/keyringfile.keylst"
+    local DEARMOR="$(dearmor_filename "$KEYRINGFILE")"
+    get_fingerprints_of_keyring "$DEARMOR" > "$FINGERPRINTS"
 
-        # strip leading 0x, if present:
-        KEY="${KEY#0x}"
+    for KEY in "$@"; do
+       # strip leading 0x, if present:
+       KEY="$(echo "${KEY#0x}" | tr -d ' ')"
 
        # check if the key is in this keyring
        if ! grep -iq "^[0-9A-F]*${KEY}$" "$FINGERPRINTS"; 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."
+           apt_warn "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
@@ -211,38 +218,55 @@ remove_key_from_keyring() {
        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"
+       if [ -L "$DEARMOR" ]; then
+           REALTARGET="$(readlink -f "$DEARMOR")"
+           mv -f "$DEARMOR" "${DEARMOR}.dpkg-tmp"
+           cp -a "$REALTARGET" "$DEARMOR"
        fi
        # delete the key from the keyring
-       aptkey_execute "$GPG_SH" --keyring "$KEYRINGFILE" --batch --delete-keys --yes "$KEY"
+       aptkey_execute "$GPG_SH" --keyring "$DEARMOR" --batch --delete-keys --yes "$KEY"
        if [ -n "$REALTARGET" ]; then
            # the real backup is the old link, not the copy we made
-           mv -f "${KEYRINGFILE}.dpkg-tmp" "${KEYRINGFILE}~"
+           mv -f "${DEARMOR}.dpkg-tmp" "${DEARMOR}~"
        fi
+       if [ "$DEARMOR" != "$KEYRINGFILE" ]; then
+           mv -f "$KEYRINGFILE" "${KEYRINGFILE}~"
+           create_new_keyring "$KEYRINGFILE"
+           aptkey_execute "$GPG_SH" --keyring "$DEARMOR" --armor --export > "$KEYRINGFILE"
+       fi
+       get_fingerprints_of_keyring "$DEARMOR" > "$FINGERPRINTS"
     done
 }
 
+accessible_file_exists() {
+   if ! test -s "$1"; then
+      return 1
+   fi
+   if test -r "$1"; then
+      return 0
+   fi
+   apt_warn "The key(s) in the keyring $1 are ignored as the file is not readable by user '$USER' executing apt-key."
+   return 1
+}
+
 foreach_keyring_do() {
    local ACTION="$1"
    shift
    # if a --keyring was given, just work on this one
    if [ -n "$FORCED_KEYRING" ]; then
-       $ACTION "$FORCED_KEYRING" "$@"
+       $ACTION "$TRUSTEDFILE" "$@"
    else
        # otherwise all known keyrings are up for inspection
-       if [ -s "$TRUSTEDFILE" ]; then
+       if accessible_file_exists "$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
            TRUSTEDPARTS="$(readlink -f "$TRUSTEDPARTS")"
-           local TRUSTEDPARTSLIST="$(cd /; find "$TRUSTEDPARTS" -mindepth 1 -maxdepth 1 -name '*.gpg')"
+           local TRUSTEDPARTSLIST="$(cd /; find "$TRUSTEDPARTS" -mindepth 1 -maxdepth 1 \( -name '*.gpg' -o -name '*.asc' \))"
            for trusted in $(echo "$TRUSTEDPARTSLIST" | sort); do
-               if [ -s "$trusted" ]; then
+               if accessible_file_exists "$trusted"; then
                    $ACTION "$trusted" "$@"
                fi
            done
@@ -250,11 +274,41 @@ foreach_keyring_do() {
    fi
 }
 
-run_cmd_on_keyring() {
+list_keys_in_keyring() {
     local KEYRINGFILE="$1"
     shift
     # fingerprint and co will fail if key isn't in this keyring
-    aptkey_execute "$GPG_SH" --keyring "$KEYRINGFILE" --batch "$@" 2>/dev/null || true
+    aptkey_execute "$GPG_SH" --keyring "$(dearmor_filename "$KEYRINGFILE")" "$@" > "${GPGHOMEDIR}/gpgoutput.log" 2> "${GPGHOMEDIR}/gpgoutput.err" || true
+    if [ ! -s "${GPGHOMEDIR}/gpgoutput.log" ]; then
+       return
+    fi
+    # we fake gpg header here to refer to the real asc file rather than a temp file
+    if [ "${KEYRINGFILE##*.}" = 'asc' ]; then
+       if expr match "$(sed -n '2p' "${GPGHOMEDIR}/gpgoutput.log")" '^-\+$' >/dev/null 2>&1; then
+           echo "$KEYRINGFILE"
+           echo "$KEYRINGFILE" | sed 's#[^-]#-#g'
+           sed '1,2d' "${GPGHOMEDIR}/gpgoutput.log" || true
+       else
+           cat "${GPGHOMEDIR}/gpgoutput.log"
+       fi
+    else
+       cat "${GPGHOMEDIR}/gpgoutput.log"
+    fi
+    if [ -s "${GPGHOMEDIR}/gpgoutput.err" ]; then
+       cat >&2 "${GPGHOMEDIR}/gpgoutput.err"
+    fi
+}
+
+export_key_from_to() {
+    local FROM="$1"
+    local TO="$2"
+    shift 2
+    if ! aptkey_execute "$GPG_SH" --keyring "$(dearmor_filename "$FROM")" --export "$@" > "$TO" 2> "${GPGHOMEDIR}/gpgoutput.log"; then
+       cat >&2 "${GPGHOMEDIR}/gpgoutput.log"
+       false
+    else
+       chmod 0644 -- "$TO"
+    fi
 }
 
 import_keyring_into_keyring() {
@@ -272,12 +326,11 @@ import_keyring_into_keyring() {
     if [ ! -s "$TO" ]; then
        if [ -s "$FROM" ]; then
            if [ -z "$2" ]; then
-               if ! aptkey_execute "$GPG_SH" --keyring "$FROM" --export ${1:+"$1"} > "$TO" 2> "${GPGHOMEDIR}/gpgoutput.log"; then
-                   cat >&2 "${GPGHOMEDIR}/gpgoutput.log"
-                   false
-               else
-                   chmod 0644 -- "$TO"
+               local OPTS
+               if [ "${TO##*.}" = 'asc' ]; then
+                   OPTS='--armor'
                fi
+               export_key_from_to "$(dearmor_filename "$FROM")" "$TO" $OPTS ${1:+"$1"}
            else
                create_new_keyring "$TO"
            fi
@@ -287,43 +340,61 @@ import_keyring_into_keyring() {
     elif [ -s "$FROM" ]; then
        local EXPORTLIMIT="$1"
        if [ -n "$1$2" ]; then shift; fi
-       if ! aptkey_execute "$GPG_SH" --keyring "$FROM" --export ${EXPORTLIMIT:+"$EXPORTLIMIT"} \
-          | aptkey_execute "$GPG_SH" --keyring "$TO" --batch --import "$@" > "${GPGHOMEDIR}/gpgoutput.log" 2>&1; then
+       local DEARMORTO="$(dearmor_filename "$TO")"
+       if ! aptkey_execute "$GPG_SH" --keyring "$(dearmor_filename "$FROM")" --export ${EXPORTLIMIT:+"$EXPORTLIMIT"} \
+          | aptkey_execute "$GPG_SH" --keyring "$DEARMORTO" --batch --import "$@" > "${GPGHOMEDIR}/gpgoutput.log" 2>&1; then
            cat >&2 "${GPGHOMEDIR}/gpgoutput.log"
            false
        fi
+       if [ "$DEARMORTO" != "$TO" ]; then
+           export_key_from_to "$DEARMORTO" "${DEARMORTO}.asc" --armor
+           if ! cmp -s "$TO" "${DEARMORTO}.asc" 2>/dev/null; then
+               cp -a "$TO" "${TO}~"
+               mv -f "${DEARMORTO}.asc" "$TO"
+           fi
+       fi
     fi
 }
 
-merge_all_trusted_keyrings_into_pubring() {
-    # does the same as:
-    # foreach_keyring_do 'import_keys_from_keyring' "${GPGHOMEDIR}/pubring.gpg"
-    # but without using gpg, just cat and find
-    local PUBRING="$(readlink -f "${GPGHOMEDIR}/pubring.gpg")"
-    # if a --keyring was given, just use this one
-    if [ -n "$FORCED_KEYRING" ]; then
-       if [ -s "$FORCED_KEYRING" ]; then
-           cp --dereference "$FORCED_KEYRING" "$PUBRING"
+dearmor_keyring() {
+    # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=831409#67
+    # The awk script is more complex through to skip surrounding garbage and
+    # to support multiple keys in one file (old gpgs generate version headers
+    # which get printed with the original and hence result in garbage input for base64
+    awk '/^-----BEGIN/{ x = 1; }
+/^$/{ if (x == 1) { x = 2; }; }
+/^[^=-]/{ if (x == 2) { print $0; }; }
+/^-----END/{ x = 0; }' | base64 -d
+}
+dearmor_filename() {
+    if [ "${1##*.}" = 'asc' ]; then
+       local trusted="${GPGHOMEDIR}/${1##*/}.gpg"
+       if [ -s "$1" ]; then
+           dearmor_keyring < "$1" > "$trusted"
        fi
+       echo "$trusted"
+    elif [ "${1##*.}" = 'gpg' ]; then
+       echo "$1"
+    elif [ "$(head -n 1 "$1" 2>/dev/null)" = '-----BEGIN PGP PUBLIC KEY BLOCK-----' ]; then
+       local trusted="${GPGHOMEDIR}/${1##*/}.gpg"
+       dearmor_keyring < "$1" > "$trusted"
+       echo "$trusted"
     else
-       # otherwise all known keyrings are merged
-       local TRUSTEDPARTS="/etc/apt/trusted.gpg.d"
-       eval $(apt-config shell TRUSTEDPARTS Dir::Etc::TrustedParts/d)
-       if [ -d "$TRUSTEDPARTS" ]; then
-           rm -f "$PUBRING"
-           if [ -s "$TRUSTEDFILE" ]; then
-               cat "$TRUSTEDFILE" > "$PUBRING"
-           fi
-           TRUSTEDPARTS="$(readlink -f "$TRUSTEDPARTS")"
-           (cd /; find "$TRUSTEDPARTS" -mindepth 1 -maxdepth 1 -name '*.gpg' -exec cat {} + >> "$PUBRING";)
-       elif [ -s "$TRUSTEDFILE" ]; then
-           cp --dereference "$TRUSTEDFILE" "$PUBRING"
-       fi
+       echo "$1"
     fi
+}
+catfile() {
+    cat "$(dearmor_filename "$1")" >> "$2"
+}
 
-    if [ ! -s "$PUBRING" ]; then
-       touch "$PUBRING"
-    fi
+merge_all_trusted_keyrings_into_pubring() {
+    # does the same as:
+    # foreach_keyring_do 'import_keys_from_keyring' "${GPGHOMEDIR}/pubring.gpg"
+    # but without using gpg, just cat and find
+    local PUBRING="$(readlink -f "${GPGHOMEDIR}")/pubring.gpg"
+    rm -f "$PUBRING"
+    touch "$PUBRING"
+    foreach_keyring_do 'catfile' "$PUBRING"
 }
 
 import_keys_from_keyring() {
@@ -337,6 +408,10 @@ merge_keys_into_keyrings() {
 merge_back_changes() {
     if [ -n "$FORCED_KEYRING" ]; then
        # if the keyring was forced merge is already done
+       if [ "$FORCED_KEYRING" != "$TRUSTEDFILE" ]; then
+           mv -f "$FORCED_KEYRING" "${FORCED_KEYRING}~"
+           export_key_from_to "$TRUSTEDFILE" "$FORCED_KEYRING" --armor
+       fi
        return
     fi
     if [ -s "${GPGHOMEDIR}/pubring.gpg" ]; then
@@ -380,6 +455,7 @@ exec sh '($(escape_shell "${GPG}")' --keyring '$(escape_shell "${TRUSTEDFILE}")'
 exec sh '$(escape_shell "${GPG}")' --keyring '$(escape_shell "${GPGHOMEDIR}/pubring.gpg")' \"\$@\"" > "${GPGHOMEDIR}/gpg.1.sh"
        GPG="${GPGHOMEDIR}/gpg.1.sh"
     else
+       TRUSTEDFILE="$(dearmor_filename "$FORCED_KEYRING")"
        create_new_keyring "$TRUSTEDFILE"
        echo "#!/bin/sh
 exec sh '$(escape_shell "${GPG}")' --keyring '$(escape_shell "${TRUSTEDFILE}")' \"\$@\"" > "${GPGHOMEDIR}/gpg.1.sh"
@@ -389,10 +465,10 @@ exec sh '$(escape_shell "${GPG}")' --keyring '$(escape_shell "${TRUSTEDFILE}")'
 
 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"
+    if ! [ -e "$1" ]; then
+       if [ -w "$(dirname "$1")" ]; then
+           touch -- "$1"
+           chmod 0644 -- "$1"
        fi
     fi
 }
@@ -473,6 +549,50 @@ if [ -z "$command" ]; then
 fi
 shift
 
+find_gpgv_status_fd() {
+   while [ -n "$1" ]; do
+       if [ "$1" = '--status-fd' ]; then
+               shift
+               echo "$1"
+               break
+       fi
+       shift
+   done
+}
+GPGSTATUSFD="$(find_gpgv_status_fd "$@")"
+
+apt_warn() {
+    if [ -z "$GPGHOMEDIR" ]; then
+       echo >&2 'W:' "$@"
+    else
+       echo 'W:' "$@" > "${GPGHOMEDIR}/aptwarnings.log"
+    fi
+    if [ -n "$GPGSTATUSFD" ]; then
+       echo >&${GPGSTATUSFD} '[APTKEY:] WARNING' "$@"
+    fi
+}
+apt_error() {
+    if [ -z "$GPGHOMEDIR" ]; then
+       echo >&2 'E:' "$@"
+    else
+       echo 'E:' "$@" > "${GPGHOMEDIR}/aptwarnings.log"
+    fi
+    if [ -n "$GPGSTATUSFD" ]; then
+       echo >&${GPGSTATUSFD} '[APTKEY:] ERROR' "$@"
+    fi
+}
+
+cleanup_gpg_home() {
+    if [ -z "$GPGHOMEDIR" ]; then return; fi
+    if [ -s "$GPGHOMEDIR/aptwarnings.log" ]; then
+       cat >&2 "$GPGHOMEDIR/aptwarnings.log"
+    fi
+    if command_available 'gpgconf'; then
+       GNUPGHOME="${GPGHOMEDIR}" gpgconf --kill gpg-agent >/dev/null 2>&1 || true
+    fi
+    rm -rf "$GPGHOMEDIR"
+}
+
 create_gpg_home() {
     # 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 later on
@@ -484,8 +604,12 @@ create_gpg_home() {
        fi
     fi
     GPGHOMEDIR="$(mktemp -d)"
-    CURRENTTRAP="${CURRENTTRAP} rm -rf '$(escape_shell "${GPGHOMEDIR}")';"
+    CURRENTTRAP="${CURRENTTRAP} cleanup_gpg_home;"
     trap "${CURRENTTRAP}" 0 HUP INT QUIT ILL ABRT FPE SEGV PIPE TERM
+    if [ -z "$GPGHOMEDIR" ]; then
+       apt_error "Could not create temporary gpg home directory in $TMPDIR (wrong permissions?)"
+       exit 28
+    fi
     chmod 700 "$GPGHOMEDIR"
 }
 
@@ -511,10 +635,10 @@ EOF
        GPG_EXE="gpg"
     elif command_available 'gpg2'; then
        GPG_EXE="gpg2"
+    elif command_available 'gpg1'; then
+       GPG_EXE="gpg1"
     else
-       echo >&2 "Error: gnupg or gnupg2 do not seem to be installed,"
-       echo >&2 "Error: but apt-key requires gnupg or gnupg2 for this operation."
-       echo >&2
+       apt_error 'gnupg, gnupg2 and gnupg1 do not seem to be installed, but one of them is required for this operation'
        exit 255
     fi
 
@@ -600,7 +724,7 @@ case "$command" in
        ;;
     list|finger*)
        warn_on_script_usage
-       foreach_keyring_do 'run_cmd_on_keyring' --fingerprint "$@"
+       foreach_keyring_do 'list_keys_in_keyring' --fingerprint "$@"
        ;;
     export|exportall)
        warn_on_script_usage
@@ -610,7 +734,7 @@ case "$command" in
     adv*)
        warn_on_script_usage
        setup_merged_keyring
-       aptkey_echo "Executing: $GPG $*"
+       aptkey_echo "Executing: $GPG" "$@"
        aptkey_execute "$GPG" "$@"
        merge_back_changes
        ;;
@@ -620,8 +744,9 @@ case "$command" in
        if [ -n "$GPGV" ] && command_available "$GPGV"; then true;
        elif command_available 'gpgv'; then GPGV='gpgv';
        elif command_available 'gpgv2'; then GPGV='gpgv2';
+       elif command_available 'gpgv1'; then GPGV='gpgv1';
        else
-          echo >&2 'ERROR: gpgv or gpgv2 required for verification'
+          apt_error 'gpgv, gpgv2 or gpgv1 required for verification, but neither seems installed'
           exit 29
        fi
        # for a forced keyid we need gpg --export, so full wrapping required
@@ -632,7 +757,7 @@ case "$command" in
        fi
        setup_merged_keyring
        if [ -n "$FORCED_KEYRING" ]; then
-           "$GPGV" --homedir "${GPGHOMEDIR}" --keyring "${FORCED_KEYRING}" --ignore-time-conflict "$@"
+           "$GPGV" --homedir "${GPGHOMEDIR}" --keyring "$(dearmor_filename "${FORCED_KEYRING}")" --ignore-time-conflict "$@"
        else
            "$GPGV" --homedir "${GPGHOMEDIR}" --keyring "${GPGHOMEDIR}/pubring.gpg" --ignore-time-conflict "$@"
        fi