git: Allow using system pinentry in gpg wrapper script (#62357)
Closes https://github.com/zed-industries/zed/issues/61806 Follow-up to #58791 and #61265. # Objective Since Zed started injecting its own gpg wrapper for commit signing, the wrapper's "silent first attempt" used `--pinentry-mode error`, which forbids gpg-agent from launching *any* pinentry. That was meant to avoid the `gpg: signing failed: Inappropriate ioctl for device` failure from TTY-based pinentries, but it also blocked **GUI** pinentries like pinentry-mac, which need no TTY and can supply the passphrase silently from the macOS Keychain. For users with that setup (and gpg-agent's in-memory cache disabled via `default-cache-ttl 0`), the first attempt always failed and Zed's passphrase modal appeared on **every commit**, even though terminal git signed silently. This also makes the setting proposed in #61533 unnecessary: instead of asking users to choose between Zed's prompt and the system pinentry, the wrapper now tries the system pinentry automatically and only falls back to Zed's askpass modal when gpg genuinely cannot obtain the passphrase on its own. ## Solution Restructure the wrapper script to sign in three stages, most silent first, so every pinentry configuration self-selects the right behavior without any detection or settings: 1. **`--pinentry-mode error`**: succeeds only via gpg-agent's passphrase cache or an unprotected key; guaranteed to never prompt anywhere. This keeps the #61265 behavior byte-identical. 2. **Default pinentry mode**: lets the configured pinentry run, exactly like terminal git. GUI pinentries (e.g. pinentry-mac reading the macOS Keychain) need no TTY and sign silently or show their native dialog; TTY pinentries fail fast with `Inappropriate ioctl for device` because git spawns gpg without a TTY. 3. **Loopback mode**: asks for the passphrase via Zed's askpass modal and hands it to gpg on fd 3, for setups where gpg cannot prompt at all (e.g. when gpg-agent's `pinentry-program` is not configured). ## Testing Initial steps: 1. Run `security delete-generic-password -s GnuPG` to remove the GnuPG keychain entry, forcing you to re-enter your passphrase (start from the initial state of issue) 2. (**optional for a few test cases**) Configure your `~/.gnupg/gpg-agent.conf` with `pinentry-program /opt/homebrew/bin/pinentry-mac` 3. Run `gpgconf --kill gpg-agent` to force reset/kill the gpg agent cache Cases to test: 1. Have no existing gpg key and you can still commit without needing to enter a passphrase 2. Have a gpg key configured in git but no `~/.gnupg/gpg-agent.conf` config that configures the `pinentry-program` This should still request your passphrase via Zed's askpass modal, even though no `pinentry-program` is configured and committing via the terminal throws an error `gpg: signing failed: Inappropriate ioctl for device` 3. Have a gpg key configured in git and a `~/.gnupg/gpg-agent.conf` config that configures the `pinentry-program`, and cancel the native pinentry program → Zed should fall back to requesting your passphrase via the askpass modal 4. Have a gpg key configured in git and a `~/.gnupg/gpg-agent.conf` config that configures the `pinentry-program`, and fill in your passphrase via the native pinentry program with storing your passphrase in the keychain **disabled** 5. Have a gpg key configured in git and a `~/.gnupg/gpg-agent.conf` config that configures the `pinentry-program`, and fill in your passphrase via the native pinentry program with storing your passphrase in the keychain **enabled** → subsequent commits sign silently, matching terminal git ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [ ] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable ## Showcase **After: (Testing that the system pinentry program is used and works without needing to reenter your passphrase everytime only when you remove it from your keychain)** https://github.com/user-attachments/assets/0c8cdd11-957d-443a-ab10-1579bbf3b49e --- Release Notes: - Git: Fixed the GPG passphrase modal appearing on every commit for users whose configured pinentry (e.g. pinentry-mac with the macOS Keychain) can supply the passphrase without Zed's help. Zed now only prompts when gpg cannot obtain the passphrase on its own. --------- Co-authored-by: Eric Holk <eric@zed.dev>
This commit is contained in:
1 parent
33b0b39e40
commit
ea68882d4b
1 file changed
+32
-13
@@ -505,15 +505,23 @@ fn generate_gpg_wrapper_script(
|
||||
.context("Failed to shell-escape gpg passphrase prompt")?;
|
||||
|
||||
// The wrapper only intervenes when git asks gpg to *sign* (e.g. `gpg -bsau
|
||||
// <key>`); other invocations like `--verify` run unchanged. For signing we
|
||||
// first try plain gpg so gpg-agent/keychain can supply a cached or empty
|
||||
// passphrase silently, and only fall back to asking Zed (loopback mode, fd
|
||||
// 3) when that fails, e.g. the "Inappropriate ioctl for device" case with no
|
||||
// TTY for pinentry.
|
||||
// <key>`); other invocations like `--verify` run unchanged. Signing runs in
|
||||
// three stages, most silent first, so every pinentry configuration works
|
||||
// without Zed getting in the way:
|
||||
// 1. `--pinentry-mode error`: succeeds only via gpg-agent's passphrase
|
||||
// cache or an unprotected key; guaranteed to never prompt anywhere.
|
||||
// 2. Default pinentry mode: lets the configured pinentry run. GUI
|
||||
// pinentries (e.g. pinentry-mac reading the macOS Keychain) need no
|
||||
// TTY and can sign silently or show their native dialog, exactly like
|
||||
// terminal git; TTY pinentries fail fast ("Inappropriate ioctl for
|
||||
// device") because git spawns gpg without a TTY.
|
||||
// 3. Loopback mode with the passphrase from Zed's askpass modal (fd 3),
|
||||
// for setups where gpg cannot prompt at all.
|
||||
// (e.g. when gpg agent's `pinentry-program` is not configured)
|
||||
//
|
||||
// git streams the payload on stdin (readable once) and reads the signature
|
||||
// from stdout, so we buffer stdin to replay it into both attempts and buffer
|
||||
// the first attempt's output, forwarding it only if it succeeds. The
|
||||
// from stdout, so we buffer stdin to replay it into each attempt and buffer
|
||||
// each attempt's output, forwarding it only if it succeeds. The
|
||||
// passphrase goes to fd 3 via a pipe.
|
||||
Ok(format!(
|
||||
r#"#!/bin/sh
|
||||
@@ -533,7 +541,7 @@ if [ -z "${{is_signing}}" ]; then
|
||||
exec {gpg_program} "$@"
|
||||
fi
|
||||
|
||||
# Signing. Buffer stdin (the payload) and the first attempt's output
|
||||
# Signing. Buffer stdin (the payload) and each attempt's output
|
||||
# so we can retry cleanly on failure without git seeing partial output.
|
||||
tmpdir=$(mktemp -d) || exit 1
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
@@ -542,17 +550,28 @@ signature="$tmpdir/signature"
|
||||
status="$tmpdir/status"
|
||||
cat > "$payload" || exit 1
|
||||
|
||||
# First try letting gpg-agent/keychain supply the passphrase without any
|
||||
# interactive pinentry. If that succeeds (cached passphrase)
|
||||
# forward its output and we're done, so Zed never shows a modal.
|
||||
# Stage 1: fully silent. Only succeeds if gpg-agent already has the passphrase
|
||||
# cached or the key is unprotected; `--pinentry-mode error` forbids launching
|
||||
# any pinentry, so this can never prompt anywhere.
|
||||
if {gpg_program} --pinentry-mode error "$@" < "$payload" > "$signature" 2> "$status"; then
|
||||
cat "$status" >&2
|
||||
cat "$signature"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# The silent attempt failed: ask Zed for the passphrase, then hand it to gpg on
|
||||
# fd 3 using loopback mode so no pinentry/terminal is required.
|
||||
# Stage 2: let gpg launch the configured pinentry, matching terminal git. GUI
|
||||
# pinentries work without a TTY and may fetch the passphrase from an OS
|
||||
# keychain silently (or show their native dialog); TTY pinentries fail fast
|
||||
# with "Inappropriate ioctl for device" since gpg has no TTY here.
|
||||
if {gpg_program} "$@" < "$payload" > "$signature" 2> "$status"; then
|
||||
cat "$status" >&2
|
||||
cat "$signature"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Stage 3: gpg cannot obtain the passphrase on its own. Ask Zed for it, then
|
||||
# hand it to gpg on fd 3 using loopback mode so no pinentry/terminal is
|
||||
# required.
|
||||
passphrase=$(printf '%s\0' {prompt} | {askpass_program} --askpass={askpass_socket} 2>/dev/null)
|
||||
printf '%s\n' "$passphrase" |
|
||||
{gpg_program} --pinentry-mode loopback --passphrase-fd 3 "$@" 3<&0 < "$payload"
|
||||
|
||||
Reference in new issue
Block a user