Subject: [bug#77667] guix-install.sh: Check fingerprint of downloaded PGP keys before importing

From: Scott Tankard <[email-masked]>
Date: Mon, 5 May 2025 16:43:37 -0700

The prior "patch" I sent was in fact a diff; this one is formatted with
`git format-patch`. Content is almost exactly the same as the prior diff.
From 03bacc6e90063a3d75365354f037dbb943b1a98e Mon Sep 17 00:00:00 2001
From: user <>
Date: Mon, 5 May 2025 16:29:31 -0700
Subject: [PATCH] guix-install.sh: Check fingerprint of downloaded PGP keys
 before importing

---
 etc/guix-install.sh | 64 ++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 55 insertions(+), 9 deletions(-)

diff --git a/etc/guix-install.sh b/etc/guix-install.sh
index b5d833c..224fa50 100755
--- a/etc/guix-install.sh
+++ b/etc/guix-install.sh
@@ -105,6 +105,12 @@ declare -A GPG_SIGNING_KEYS
 GPG_SIGNING_KEYS[15145]=3CE464558A84FDC69DB40CFB090B11993D9AEBB5  # ludo
 GPG_SIGNING_KEYS[127547]=27D586A4F8900854329FF09F1260E46482E63562 # maxim
 
+# TEST_FAIL_FETCH_KEY=no
+# if [ "${TEST_FAIL_FETCH_KEY}" = 'yes' ]; then
+# 	GPG_SIGNING_KEYS[15145]=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+# 	GPG_SIGNING_KEYS[127547]=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
+# fi
+
 # ------------------------------------------------------------------------------
 #+UTILITIES
 
@@ -189,12 +195,49 @@ add_init_sys_require()
     fi
 }
 
+get_gpg_fpr_of_keyfile()
+{
+    local keyfile="$1"
+    local tmp_dir
+    tmp_dir="$(mktemp -t -d guix.gnupg.tmphomedir.XXXXXX)"
+    gpg -qq --homedir "$tmp_dir" --dry-run --no-keyring \
+        --with-colons --show-keys "$keyfile" \
+        | grep '^pub:' -A 1 | tail -n 1 \
+        | grep -oE '^([^:]*:){10}' | grep -oE '[^:]*:$' | tr -d ':'
+    # awk -F: '/^pub:/ { getline; print $10 }'
+
+    # Explanation: Get the line immediately after the line that starts
+    # with `pub:`. Then get the 10th field from that line, with colons as
+    # field separators.
+}
+
+# Check that a keyfile has expected fingerprint and import it to GPG keyring
+# only if so. If keyfile path is given as '-', it reads from STDIN. Returns 0
+# or 1 depending in its outcome, so it can be used in condition tests.
+import_key()
+{
+    local keyfile="$1"
+    local fpr="$2"
+    local tmp_dir
+
+    [ "$keyfile" = "-" ] && keyfile=/dev/stdin
+    tmp_dir="$(mktemp -t -d guix.gpg.XXXXXX)"
+    cp "$keyfile" "$tmp_dir/key.gpg"
+    # Copying to a temp dir ensures no unprivileged process can tamper
+    # with it between the time we check the fingerprint and gpg-import it.
+    if [ "$fpr" = "$(get_gpg_fpr_of_keyfile "$tmp_dir/key.gpg")" ]; then
+        gpg --import "$tmp_dir/key.gpg" && return 0
+    fi
+    return 1
+}
+
 chk_gpg_keyring()
 { # Check whether the Guix release signing public key is present.
     _debug "--- [ ${FUNCNAME[0]} ] ---"
     local user_id
     local gpg_key_id
     local exit_flag
+    local tmp_dir
 
     for user_id in "${!GPG_SIGNING_KEYS[@]}"; do
         gpg_key_id=${GPG_SIGNING_KEYS[$user_id]}
@@ -209,17 +252,19 @@ Would you like me to fetch it for you?"; then
             # Use a reasonable time-out here so users don't report silent
             # ‘freezes’ when Savannah goes out to lunch, as has happened.
             if wget "https://sv.gnu.org/people/viewgpg.php?user_id=$user_id" \
-                    --timeout=30 --no-verbose -O- | gpg --import -; then
+                --timeout=30 --no-verbose -O - | import_key - "$gpg_key_id"; then
                 continue
             fi
         fi
-	# If we reach this point, the key is (still) missing.  Report further
-	# missing keys, if any, but then abort the installation.
+        # If we reach this point, the key is (still) missing.  Report further
+        # missing keys, if any, but then abort the installation.
         _err "Missing OpenPGP public key ($gpg_key_id).
 Fetch it with this command:
 
-  wget \"https://sv.gnu.org/people/viewgpg.php?user_id=$user_id\" -O - | \
-sudo -i gpg --import -"
+wget 'https://sv.gnu.org/people/viewgpg.php?user_id=${user_id}' -O - | \
+sudo sh $0 import_key - '$gpg_key_id'
+
+"
         exit_flag=yes
     done
     if [ "$exit_flag" = yes ]; then
@@ -1030,14 +1075,15 @@ main_uninstall()
 
 main()
 {
-    # expect no parameters
-    # or '--uninstall'
     if [ 0 -eq $# ]; then
         main_install
     else
-        local uninstall_flag="$1"
-        if [ '--uninstall' = "${uninstall_flag}" ]; then
+        local _flag="$1"
+        if [ "${_flag}" = '--uninstall' ]; then
             main_uninstall
+        elif [ "${_flag}" = "import_key" ]; then
+            import_key "$2" "$3" && exit 0
+            exit 1
         else
             echo "unsupported parameters: $*"
             exit 1
-- 
2.45.0