Skip to content

fix(ssh): stop the askpass helper answering security-key prompts with a passphrase - #127

Merged
TsekaLuk merged 1 commit into
mainfrom
fix/ssh-askpass-prompt-kind-80
Aug 10, 2026
Merged

fix(ssh): stop the askpass helper answering security-key prompts with a passphrase#127
TsekaLuk merged 1 commit into
mainfrom
fix/ssh-askpass-prompt-kind-80

Conversation

@TsekaLuk

Copy link
Copy Markdown
Contributor

Part of #80 (FIDO2/OpenSSH transport gating).

The bug

Pebble sets SSH_ASKPASS_REQUIRE=force (ssh_agent_hook_bootstrap.go:203) so an unattended probe can answer a key passphrase without a TTY. That setting routes every OpenSSH prompt through the helper — and the helper printed the secret unconditionally:

if os.Getenv("PEBBLE_SSH_ASKPASS_MODE") == "1" {
    fmt.Fprintln(os.Stdout, os.Getenv("PEBBLE_SSH_ASKPASS_SECRET"))
    return
}

It never looked at os.Args[1], which is where OpenSSH puts the prompt text.

So a FIDO2 target asked Enter PIN for ED25519-SK key ... received the cached passphrase. The wrong secret goes to a different authenticator, the attempt is burnt, and the user sees a passphrase error instead of a request to touch the key. Keyboard-interactive challenges (One-time password:, Verification code:) were answered the same way.

This branch of main.go had no test coverage at all — the existing askpass test reads PEBBLE_SSH_ASKPASS_SECRET from the environment inside a fake ssh script and never invokes the helper.

The fix

Classify the prompt; release the credential only for a passphrase or account-password prompt, and exit non-zero otherwise. Exiting zero with no output would instead offer OpenSSH an empty password.

The distinction is drawn on the wording, not the key type, which is the part that is easy to get wrong:

prompt classification why
Enter passphrase for ED25519-SK key ... secret decrypts the local key file — the cached credential is the right answer
Enter PIN for ED25519-SK key ... security key the FIDO2 PIN, held by the authenticator
Confirm user presence for key ... security key a touch request, not a secret
pebble@host's password: secret OpenSSH's account prompt
One-time password: other a challenge; contains "password" but must not be answered

Unrecognised prompts fail closed.

Host-key prompts are unaffected: both call sites already set StrictHostKeyChecking=accept-new (ssh_agent_hook_bootstrap.go:157, ssh_text_generation_relay.go:305), so the authenticity question never reaches askpass. I checked this before changing anything, since failing closed there would have broken first contact with a new host.

Verification

  • Three table tests using OpenSSH's own prompt wording, so a future rewording surfaces here rather than as a hardware-key target silently getting the wrong secret.
  • Mutation-tested with 6 mutants; 5 caught. The survivor — reordering the passphrase check ahead of the security-key check — is an equivalent mutant: no prompt OpenSSH actually emits contains both "passphrase" and the PIN/presence wording, so the order is defensive rather than load-bearing. I did not invent a prompt OpenSSH never sends in order to kill it.
  • Full go test ./... -race green, matching the baseline on main.

Note on scope

This is one of the defects behind #80, not the whole issue. #80 also wants a capability probe (ssh -Q key for sk-* support) so FIDO2 is only offered when the local OpenSSH supports it, and actionable install guidance behind the eight bare system ssh binary not found errors. Those are separate changes.

🤖 Generated with Claude Code

… a passphrase

Pebble sets SSH_ASKPASS_REQUIRE=force so an unattended probe can answer a
key passphrase without a TTY. That setting routes *every* OpenSSH prompt
through the helper, and the helper printed PEBBLE_SSH_ASKPASS_SECRET
unconditionally — it never looked at argv[1], where OpenSSH puts the
prompt text.

So a FIDO2 target asked "Enter PIN for ED25519-SK key ..." received the
cached passphrase. The wrong secret goes to a different authenticator,
the attempt is burnt, and the user sees a passphrase error rather than a
request to touch the key. Keyboard-interactive challenges such as
"One-time password:" were answered the same way.

Classify the prompt and release the credential only for a passphrase or
account-password prompt, exiting non-zero otherwise. Exiting zero with no
output would instead offer OpenSSH an empty password.

The distinction is on the wording, not the key type: "Enter PIN for
ED25519-SK key" is the FIDO2 PIN, while "Enter passphrase for ED25519-SK
key" decrypts the local key file and the cached credential is the correct
answer for it.

Host-key prompts are unaffected — both call sites already set
StrictHostKeyChecking=accept-new, so the authenticity question never
reaches askpass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@TsekaLuk
TsekaLuk force-pushed the fix/ssh-askpass-prompt-kind-80 branch from ae4f5bb to e771138 Compare August 10, 2026 02:44
@TsekaLuk
TsekaLuk merged commit 3af9964 into main Aug 10, 2026
6 of 8 checks passed
@TsekaLuk
TsekaLuk deleted the fix/ssh-askpass-prompt-kind-80 branch August 10, 2026 03:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant