Failure using pinentry-tty or pinentry-curses when GnuPG's standard input is a file
Open, Needs TriagePublic

Description

Subkeys exported with --export-secret-keys will be imported successfully with --import but the subkeys will not actually work. If GnuPG's input is provided via standard input then it fails to run versions of pinentry that require use of the terminal. The error reported by GnuPG doesn't clearly indicate that pinentry failed and instead it reports that the secret key is missing.

For pinentry-tty, the error message is:

gpg: public key decryption failed: Invalid IPC response
gpg: decryption failed: No secret key

For pinentry-curses, the error message is:

gpg: public key decryption failed: Inappropriate ioctl for device
gpg: decryption failed: No secret key

The latter is the more useful message because it reveals that pinentry-curses tried to use the input file as a terminal. Using strace shows this (ioctl on fd 0):

ioctl(0, TCGETS, 0x7ffc0fba08d0)        = -1 ENOTTY (Inappropriate ioctl for device)

Below is a script demonstrates the issue using --batch, but I can reproduce it via regular, interactive GnuPG commands. In my script I just use different homedirs to simulate different hosts. Also, I'm using Curve25519 so the example runs faster, but the bug isn't limited to this key type.

A host named "keygen" generates a key and exports it. Another host named "friend" encrypts a message. A third host named "import" imports the secret key and decrypts the message.

#!/bin/sh

set -e

gpg="gpg --quiet --homedir . --batch"

keygen="$(mktemp -d "keygen-XXXXXX")"
friend="$(mktemp -d "friend-XXXXXX")"
import="$(mktemp -d "import-XXXXXX")"
chmod 700 "$keygen" "$friend" "$import"
cleanup() {
    rm -rf -- "$keygen" "$friend" "$import"
}
trap cleanup INT TERM EXIT

pass="$1"

(
    cd "$keygen"
    $gpg --passphrase '' --quick-generate-key x ed25519 default none
    fpr="$($gpg --list-keys --with-colons | awk -F: '/^fpr/{print $10; exit}')"
    $gpg --passphrase "$pass" --quick-add-key "$fpr" cv25519 default none
    $gpg --list-secret-keys
    $gpg --export >../public.gpg
    echo $pass | $gpg --passphrase-fd 0 --pinentry-mode loopback \
        --export-secret-keys >../secret.gpg
)

(
    cd "$friend"
    $gpg --import ../public.gpg
    echo "Let's meet at midnight" \
        | $gpg --trust-model always --encrypt --recipient x >../msg.gpg
)

(
    cd "$import"
    echo $pass | $gpg --passphrase-fd 0 --pinentry-mode loopback \
        --import ../secret.gpg
    $gpg --list-secret-keys
    $gpg --decrypt <../msg.gpg
)

When the script is run without arguments, the subkey is unprotected. In this case everything works fine:

$ sh example.sh
keygen-UBWzBs/pubring.kbx
--------------------------------------------------------------------------------
sec   ed25519 2019-08-12 [SC]
      6DFDBB761EE6AED9F1303E1139FC599588A3C825
uid           [ultimate] x
ssb   cv25519 2019-08-12 [E]

import-hKIkzV/pubring.kbx
--------------------------------------------------------------------------------
sec   ed25519 2019-08-12 [SC]
      6DFDBB761EE6AED9F1303E1139FC599588A3C825
uid           [ unknown] x
ssb   cv25519 2019-08-12 [E]

Let's meet at midnight

When a passphrase is set and pinentry is configured as pinentry-tty or pinentry-curses:

$ sh example.sh foo
keygen-UBWzBs/pubring.kbx
--------------------------------------------------------------------------------
sec   ed25519 2019-08-12 [SC]
      422C5F8B9CBDF139C765B60774C46E98E616C620
uid           [ultimate] x
ssb   cv25519 2019-08-12 [E]

import-hKIkzV/pubring.kbx
--------------------------------------------------------------------------------
sec   ed25519 2019-08-12 [SC]
      422C5F8B9CBDF139C765B60774C46E98E616C620
uid           [ unknown] x
ssb   cv25519 2019-08-12 [E]

gpg: decryption failed: No secret key

It tries to prompt for the protection passphrase on the --decrypt line but fails.

Details

Version
2.2.17
skeeto created this task.Mon, Aug 12, 6:16 PM
gniibe added a subscriber: gniibe.Tue, Aug 13, 4:08 AM

For my environment (Debian buster's 2.2.12 and another one from GnuPG master), both (no argument and foo) work well.
The invocation with argument let pinentry pop up to ask passphrase.

Could you please check your pinentry configuration?

When I modified like this:

--- example-orig.sh	2019-08-13 10:53:21.968998211 +0900
+++ example.sh	2019-08-13 11:02:32.437156261 +0900
@@ -38,5 +38,6 @@
     echo $pass | $gpg --passphrase-fd 0 --pinentry-mode loopback \
         --import ../secret.gpg
     $gpg --list-secret-keys
-    $gpg --decrypt <../msg.gpg
+    echo $pass | $gpg --passphrase-fd 0 --pinentry-mode loopback \
+        --decrypt ../msg.gpg
 )

pinentry is not popped-up.

Those changes make the script work for me, specifically passing the input as an argument and not through standard input. Digging more, it looks like the underlying issue is related to using pinentry-tty (my case) or pinentry-curses when passing the OpenPGP input via standard input. This causes pinentry to give up before prompting. For pinentry-tty it fails with "ERR 83886340 Invalid IPC response" and pinentty-curses fails with "ERR 83918950 Inappropriate ioctl for device".

In other words, this command only works when using a GUI-style pinentry:

$ gpg --decrypt <message.gpg

But this works either way since it doesn't tie up standard input:

$ gpg --decrypt message.gpg

I'd expect pinentry-tty/-curses to work in the first case since it would use /dev/tty directly, not rely on standard input/output as the tty.

dkg added a subscriber: dkg.Tue, Aug 20, 2:45 AM

@skeeto can you edit the summary/title of this ticket to better reflect what you think the underlying issue is?

skeeto renamed this task from GnuPG unable to use imported protected subkeys to Failure using pinentry-tty or pinentry-curses when GnuPG's standard input is a file.Wed, Aug 21, 2:22 AM
skeeto updated the task description. (Show Details)

@dkg, I changed the title and adjusted the description to more accurately describe the situation.