Skip to content

Add client-side keyboard-interactive authentication (RFC 4256) - #242

Open
noelmom wants to merge 2 commits into
apple:mainfrom
noelmom:feature/keyboard-interactive-auth
Open

Add client-side keyboard-interactive authentication (RFC 4256)#242
noelmom wants to merge 2 commits into
apple:mainfrom
noelmom:feature/keyboard-interactive-auth

Conversation

@noelmom

@noelmom noelmom commented Jul 1, 2026

Copy link
Copy Markdown

Motivation

Resolves #136. NIOSSH had no support for SSH keyboard-interactive user authentication (RFC 4256), which is required to authenticate against servers that drive password/OTP/2FA prompts through PAM. This adds client-side support; server-side is intentionally left for a follow-up.

Changes

Public API

  • NIOSSHAvailableUserAuthenticationMethods.keyboardInteractive (parsed/serialized as keyboard-interactive). Excluded from .all because there is no server-side support yet, but offered on the client's initial optimistic request.
  • NIOSSHUserAuthenticationOffer.Offer.keyboardInteractive(_:) with Offer.KeyboardInteractive(languageTag:submethods:) (both default "").
  • NIOSSHKeyboardInteractiveChallenge / NIOSSHKeyboardInteractivePrompt model types.
  • NIOSSHClientUserAuthenticationDelegate.respondToKeyboardInteractiveChallenge(_:responsePromise:), with a default implementation that fails the attempt — existing delegates remain source-compatible.
  • NIOSSHError.ErrorType.unsupportedUserAuthenticationMethod, .invalidKeyboardInteractiveResponse.

Internals

  • SSH_MSG_USERAUTH_INFO_REQUEST (60) and SSH_MSG_USERAUTH_INFO_RESPONSE (61) message coding, plus the keyboard-interactive method on USERAUTH_REQUEST.
  • Identifier 60 is overloaded with SSH_MSG_USERAUTH_PK_OK. It is disambiguated from the authoritative auth state (UserAuthenticationStateMachine.expectingKeyboardInteractiveInfoRequest, true only while awaiting the outcome of a keyboard-interactive attempt), re-synced onto the packet parser immediately before every inbound read so decoding is correct independent of write/read ordering.
  • The client user-auth state machine handles any number of challenge/response rounds, including zero-prompt requests, and fails cleanly on a response-count mismatch or a failed delegate promise.
  • Existing password, public-key, and host-based auth are unchanged.

Client usage

func respondToKeyboardInteractiveChallenge(
    _ challenge: NIOSSHKeyboardInteractiveChallenge,
    responsePromise: EventLoopPromise<[String]>
) {
    let responses = challenge.prompts.map { prompt -> String in
        let p = prompt.prompt.lowercased()
        if p.contains("password") { return password }
        if p.contains("verification") || p.contains("otp") || p.contains("code") { return otp }
        return ""
    }
    responsePromise.succeed(responses) // exactly one response per prompt, in order
}

Per RFC 4256, prompts with echo == false are sensitive; responses are never logged anywhere in the implementation.

Testing

  • Message coding round-trips: keyboard-interactive request, and info requests with zero / one / many prompts, info responses with zero / many responses (including partial-read handling).
  • Overloaded identifier 60 decodes as PK_OK by default and as INFO_REQUEST when a keyboard-interactive attempt is in flight — over both plaintext and encrypted transports.
  • Method negotiation parses/serializes keyboard-interactive.
  • State machine: single round, multiple rounds (incl. zero-prompt), failure falling through to the next method, response-count mismatch failure, and the default-delegate failure path.
  • All 340 tests pass; swift format (strict lint) clean.

Limitations

Client-only. There is no NIOSSH server that emits INFO_REQUEST, so a full two-party end-to-end test isn't included; the client path is covered by encrypted parser and state-machine tests. Server-side keyboard-interactive can follow in a later change.

Noelmo Melo added 2 commits June 30, 2026 20:29
Implements client support for SSH `keyboard-interactive` user
authentication, resolving apple#136.

- Add `.keyboardInteractive` to `NIOSSHAvailableUserAuthenticationMethods`
  (parsed/serialized as `keyboard-interactive`). It is excluded from
  `.all` because server-side support is not yet implemented, but is
  offered to the client on its initial optimistic request.
- Add a `.keyboardInteractive(languageTag:submethods:)` client auth offer.
- Add public `NIOSSHKeyboardInteractiveChallenge` /
  `NIOSSHKeyboardInteractivePrompt` model types.
- Add a `respondToKeyboardInteractiveChallenge(_:responsePromise:)`
  delegate callback with a default implementation that fails the attempt,
  preserving source compatibility for existing delegates.
- Add `SSH_MSG_USERAUTH_INFO_REQUEST` (60) and
  `SSH_MSG_USERAUTH_INFO_RESPONSE` (61) message coding. Identifier 60 is
  overloaded with `SSH_MSG_USERAUTH_PK_OK`; the packet parser now decodes
  it context-sensitively based on the in-flight auth method, driven by the
  connection state machine.
- Extend the client user-auth state machine to handle any number of
  challenge/response rounds, including zero-prompt requests, and to fail
  cleanly on a response-count mismatch.
- Preserve existing password, public-key, and host-based auth behaviour.

Adds unit and integration-style tests covering method negotiation,
message coding, overloaded identifier-60 decoding, and single/multi-round
client flows.
The overloaded message identifier 60 (SSH_MSG_USERAUTH_PK_OK vs
SSH_MSG_USERAUTH_INFO_REQUEST) was disambiguated by speculatively setting
a parser flag when the outbound keyboard-interactive request was written.
That coupled correct decoding to write/read ordering.

Derive the decision from the authoritative user-auth state instead:
`UserAuthenticationStateMachine.expectingKeyboardInteractiveInfoRequest`
is true exactly while the client awaits the outcome of a
keyboard-interactive attempt. The connection state machine re-syncs the
parser from it immediately before every inbound read, so identifier 60 is
always interpreted correctly regardless of ordering.

Adds an encrypted parser round-trip test for identifier 60 and state
machine assertions covering the expectation flag across a full
keyboard-interactive attempt and a password attempt.
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.

Support keyboard-interactive authentication

1 participant