fix(ssh): report the SSH agent's own failure instead of prompting for a key passphrase (#2583) - #2584
Merged
Merged
Conversation
… a key passphrase (#2583)
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2583.
The bug
Choosing SSH Agent and connecting put up TablePro's own SSH Key Passphrase Required dialog for a private key on disk, instead of 1Password's approval sheet. The agent was never asked to sign anything.
Root cause
LibSSH2TunnelFactory.buildAuthenticatorcomposed the SSH Agent method asand
CompositeAuthenticatorswallows each step's failure to try the next. So an agent that produced nothing was silently replaced by private-key auth against a key file TablePro had guessed, and an encrypted one raisedPromptPassphraseProvider. The agent's own failure reached OSLog and nowhere else.The usual way in is leaving Agent Socket on its default SSH_AUTH_SOCK. For a GUI-launched app that variable comes from launchd and names the
ssh-agentmacOS runs, never 1Password's socket, whatever a shell profile exports.AgentAuthenticator.resolveSocketViaLaunchctl()(from #729) only runs when the variable is absent, which under launchd it is not.A second layer made it worse: when every step failed, the reported error was the last one, and
KeyboardInteractiveAuthenticatorthrew.passwordeven on a server that issued no prompt at all. A connection with no password reported SSH password rejected.Measured
A C probe against the shipped
Libs/libssh2_arm64.a(libssh2 1.11.1):SSH_AUTH_SOCKdefault agent on this Macsun_pathoverflow~/Library/...pathagent_connect rc=-42, so tilde expansion is load-bearingThe last row sent me to
buildJumpAuthenticator, which passesresolved.agentSocketPathunexpanded. That turned out not to be a bug:AgentAuthenticator.authenticateexpands it itself. No change there.The fix
SSH Agent means the agent. The chain is now
[AgentAuthenticator, KeyboardInteractiveAuthenticator]. No private key file is tried, so nothing can substitute a credential the user did not choose. Keyboard-interactive stays, because it is a second factor the same server asked for (AuthenticationMethods publickey,keyboard-interactive), not another credential.The agent's failure has a name.
AgentAuthenticatornow distinguishes three cases instead of one generic string:.agentUnavailable.agentNoIdentities.agentRejectedAnd it names the socket the connect actually used.
agentSocketPathcollapses three sources into one string, and each is changed somewhere different, so a message that assumes one of them misdirects the other two.AgentSocketOriginis resolved alongside the path inSSHConfigResolverand carried on the reason:.agentSocketSetting.identityAgentDirectiveIdentityAgentline for that host in~/.ssh/config.environmentSSH_AUTH_SOCKThat matters for two supported setups the first draft got wrong: a jump host has no Agent Socket field at all, and
ssh-addonly loads keys into theSSH_AUTH_SOCKagent, so suggesting it to a 1Password user is bad advice. It is now offered for that origin alone.An unreachable agent ends the chain. The keyboard-interactive step is a second factor for a first the agent supplies; with no agent there is no first factor, so on a server that offers keyboard-interactive it would have prompted for a credential of its own, which is the reported defect in a different dialog.
CompositeAuthenticator.endsChainOnstops the chain on.agentUnavailableand.agentNoIdentities..agentRejecteddoes not end it, because that is the partial-success case #1920 added the step for.A step the server never engaged no longer buries a real failure. New
.methodUnavailable, thrown byKeyboardInteractiveAuthenticatorwhen the server issued no prompt and nothing was answered, andCompositeAuthenticatorwill not let one displace an already-recorded failure. #1018's behaviour is preserved: a keyboard-interactive step that did answer a challenge still wins, sopublickey,keyboard-interactivestill reports.verificationCode.The Agent Socket picker says what each option reaches. The static caption under it becomes an option-specific line from
SSHAgentSocketOption.explanation, shared by the connection form and the SSH profile editor.canPromptonKeyFileAuthenticatorwastrueat every call site once the agent fallback went, so it is gone.Behaviour change
An SSH Agent connection that used to fall back to an
IdentityFilefrom~/.ssh/configand succeed there now fails with the agent's reason. That is the point: the fallback is what produced the reported bug. The error says what to change, and Private Key is the method for authenticating with a key file.Screenshots
Not captured. The visible delta is one caption line under the Agent Socket picker, replacing "Keys are provided by the SSH agent (e.g. 1Password, ssh-agent)." with, for the default option, "The ssh-agent macOS runs, from SSH_AUTH_SOCK. 1Password and Secretive listen elsewhere." Driving a sandboxed Debug build to the SSH Tunnel pane needs several AppleScript steps against a SwiftUI sheet whose rows accessibility cannot press, and the run dismissed the window before reaching the pane. Everything else in this change is an authentication chain and an error string, which a screenshot cannot show.
Verification
verify.sh generateverify.sh buildverify.sh test(13 suites)verify.sh lint TablePro TableProTestsdocs/scripts/check-writing-style.shdocs/scripts/check-docs-against-source.pySuites run:
AgentAuthenticatorFailureTests,CompositeAuthenticatorFailureReportingTests,KeyboardInteractiveFailureReasonTests,BuildAuthenticatorTests,AuthFailureReasonTests,CompositeAuthenticatorCancellationTests,KeyboardInteractiveContextTests,KeyboardInteractiveResponsesTests,SSHConfigurationTests,SSHTunnelErrorTests,SSHConfigResolverTests,ConnectionURLFormatterTests,ConnectionURLParserTests.The two agent cases run against a real unix socket speaking the ssh-agent protocol, on a libssh2 session with no transport, because the distinction under test is what libssh2 does with the bytes on that socket.
No UI automation: reproducing this needs a real SSH server and a real agent that refuses, so it does not run deterministically in
TableProUITests.Review
Reviewed by Codex (
review --scope working-tree), which raised two P2s, both fixed before the commit:libssh2_initdirectly, and libssh2's header says it uses global state and must not be called concurrently. Swift Testing runs sibling cases in parallel, so four calls could enter it together. They now go through the app's one lazyLibSSH2TunnelFactory.initialized.IdentityAgent, an inheritedSSH_AUTH_SOCK, and a jump-host socket are all supported. That is whatAgentSocketOriginabove is for.The chain-ending behaviour is mine, found while reading the review's reasoning about partial authentication.
Not changed
iOS.
TableProMobile'sSSHTunnelFactoryimplements.password,.privateKeyand.noneonly, and throws for.sshAgent.