1
0
mirror of https://git.tartarus.org/simon/putty.git synced 2025-01-09 01:18:00 +00:00

New feature: k-i authentication helper plugins.

In recent months I've had two requests from different people to build
support into PuTTY for automatically handling complicated third-party
auth protocols layered on top of keyboard-interactive - the kind of
thing where you're asked to enter some auth response, and you have to
refer to some external source like a web server to find out what the
right response _is_, which is a pain to do by hand, so you'd prefer it
to be automated in the SSH client.

That seems like a reasonable thing for an end user to want, but I
didn't think it was a good idea to build support for specific
protocols of that kind directly into PuTTY, where there would no doubt
be an ever-lengthening list, and maintenance needed on all of them.

So instead, in collaboration with one of my correspondents, I've
designed and implemented a protocol to be spoken between PuTTY and a
plugin running as a subprocess. The plugin can opt to handle the
keyboard-interactive authentication loop on behalf of the user, in
which case PuTTY passes on all the INFO_REQUEST packets to it, and
lets it make up responses. It can also ask questions of the user if
necessary.

The protocol spec is provided in a documentation appendix. The entire
configuration for the end user consists of providing a full command
line to use as the subprocess.

In the contrib directory I've provided an example plugin written in
Python. It gives a set of fixed responses suitable for getting through
Uppity's made-up k-i system, because that was a reasonable thing I
already had lying around to test against. But it also provides example
code that someone else could pick up and insert their own live
response-provider into the middle of, assuming they were happy with it
being in Python.
This commit is contained in:
Simon Tatham 2022-09-01 19:38:46 +01:00
parent 1f32a16dc8
commit 15f097f399
12 changed files with 1309 additions and 46 deletions

View File

@ -2899,8 +2899,8 @@ void setup_config_box(struct controlbox *b, bool midsession,
conf_checkbox_handler,
I(CONF_try_ki_auth));
s = ctrl_getset(b, "Connection/SSH/Auth", "params",
"Authentication parameters");
s = ctrl_getset(b, "Connection/SSH/Auth", "aux",
"Other authentication-related options");
ctrl_checkbox(s, "Allow agent forwarding", 'f',
HELPCTX(ssh_auth_agentfwd),
conf_checkbox_handler, I(CONF_agentfwd));
@ -2908,6 +2908,12 @@ void setup_config_box(struct controlbox *b, bool midsession,
HELPCTX(ssh_auth_changeuser),
conf_checkbox_handler,
I(CONF_change_username));
ctrl_settitle(b, "Connection/SSH/Auth/Credentials",
"Credentials to authenticate with");
s = ctrl_getset(b, "Connection/SSH/Auth/Credentials", "publickey",
"Public-key authentication");
ctrl_filesel(s, "Private key file for authentication:", 'k',
FILTER_KEY_FILES, false, "Select private key file",
HELPCTX(ssh_auth_privkey),
@ -2917,6 +2923,11 @@ void setup_config_box(struct controlbox *b, bool midsession,
HELPCTX(ssh_auth_cert),
conf_filesel_handler, I(CONF_detached_cert));
s = ctrl_getset(b, "Connection/SSH/Auth/Credentials", "plugin",
"Plugin to provide authentication responses");
ctrl_editbox(s, "Plugin command to run", NO_SHORTCUT, 100,
HELPCTX(ssh_auth_plugin),
conf_editbox_handler, I(CONF_auth_plugin), ED_STR);
#ifndef NO_GSSAPI
/*
* Connection/SSH/Auth/GSSAPI, which sadly won't fit on

287
contrib/authplugin-example.py Executable file
View File

@ -0,0 +1,287 @@
#!/usr/bin/env python3
# This is a demonstration example of how to write a
# keyboard-interactive authentication helper plugin using PuTTY's
# protocol for involving it in SSH connection setup.
import io
import os
import struct
import sys
# Exception class we'll use to get a clean exit on EOF.
class PluginEOF(Exception): pass
# ----------------------------------------------------------------------
#
# Marshalling and unmarshalling routines to write and read the
# necessary SSH data types to/from a binary file handle (which can
# include an io.BytesIO if you need to encode/decode in-process).
#
# Error handling is a totally ad-hoc mixture of 'assert' and just
# assuming things will have the right type, or be the right length of
# tuple, or be valid UTF-8. So it should be _robust_, in the sense
# that you'll get a Python exception if anything fails. But no
# sensible error reporting or recovery is implemented.
#
# That should be good enough, because PuTTY will log the plugin's
# standard error in its Event Log, so if the plugin crashes, you'll be
# able to retrieve the traceback.
def wr_byte(fh, b):
assert 0 <= b < 0x100
fh.write(bytes([b]))
def wr_boolean(fh, b):
wr_byte(fh, 1 if b else 0)
def wr_uint32(fh, u):
assert 0 <= u < 0x100000000
fh.write(struct.pack(">I", u))
def wr_string(fh, s):
wr_uint32(fh, len(s))
fh.write(s)
def wr_string_utf8(fh, s):
wr_string(fh, s.encode("UTF-8"))
def rd_n(fh, n):
data = fh.read(n)
if len(data) < n:
raise PluginEOF()
return data
def rd_byte(fh):
return rd_n(fh, 1)[0]
def rd_boolean(fh):
return rd_byte(fh) != 0
def rd_uint32(fh):
return struct.unpack(">I", rd_n(fh, 4))[0]
def rd_string(fh):
length = rd_uint32(fh)
return rd_n(fh, length)
def rd_string_utf8(fh):
return rd_string(fh).decode("UTF-8")
# ----------------------------------------------------------------------
#
# Protocol definitions.
our_max_version = 2
PLUGIN_INIT = 1
PLUGIN_INIT_RESPONSE = 2
PLUGIN_PROTOCOL = 3
PLUGIN_PROTOCOL_ACCEPT = 4
PLUGIN_PROTOCOL_REJECT = 5
PLUGIN_AUTH_SUCCESS = 6
PLUGIN_AUTH_FAILURE = 7
PLUGIN_INIT_FAILURE = 8
PLUGIN_KI_SERVER_REQUEST = 20
PLUGIN_KI_SERVER_RESPONSE = 21
PLUGIN_KI_USER_REQUEST = 22
PLUGIN_KI_USER_RESPONSE = 23
# ----------------------------------------------------------------------
#
# Classes to make it easy to construct and receive messages.
#
# OutMessage is constructed with the message type; then you use the
# wr_foo() routines to add fields to it, and finally call its send()
# method.
#
# InMessage is constructed via the expect() class method, to which you
# give a list of message types you expect to see one of at this stage.
# Once you've got one, you can rd_foo() fields from it.
class OutMessage:
def __init__(self, msgtype):
self.buf = io.BytesIO()
wr_byte(self.buf, msgtype)
self.write = self.buf.write
def send(self, fh=sys.stdout.buffer):
wr_string(fh, self.buf.getvalue())
fh.flush()
class InMessage:
@classmethod
def expect(cls, expected_types, fh=sys.stdin.buffer):
self = cls()
self.buf = io.BytesIO(rd_string(fh))
self.msgtype = rd_byte(self.buf)
self.read = self.buf.read
if self.msgtype not in expected_types:
raise ValueError("received packet type {:d}, expected {}".format(
self.msgtype, ",".join(map("{:d}".format,
sorted(expected_types)))))
return self
# ----------------------------------------------------------------------
#
# The main implementation of the protocol.
def protocol():
# Start by expecting PLUGIN_INIT.
msg = InMessage.expect({PLUGIN_INIT})
their_version = rd_uint32(msg)
hostname = rd_string_utf8(msg)
port = rd_uint32(msg)
username = rd_string_utf8(msg)
print(f"Got hostname {hostname!r}, port {port!r}", file=sys.stderr)
# Decide which protocol version we're speaking.
version = min(their_version, our_max_version)
assert version != 0, "Protocol version 0 does not exist"
if "TESTPLUGIN_INIT_FAIL" in os.environ:
# Test the plugin failing at startup time.
msg = OutMessage(PLUGIN_INIT_FAILURE)
wr_string_utf8(msg, os.environ["TESTPLUGIN_INIT_FAIL"])
msg.send()
return
# Send INIT_RESPONSE, with our protocol version and an overridden
# username.
#
# By default this test plugin doesn't override the username, but
# you can make it do so by setting TESTPLUGIN_USERNAME in the
# environment.
msg = OutMessage(PLUGIN_INIT_RESPONSE)
wr_uint32(msg, version)
wr_string_utf8(msg, os.environ.get("TESTPLUGIN_USERNAME", ""))
msg.send()
# Outer loop run once per authentication protocol.
while True:
# Expect a message telling us what the protocol is.
msg = InMessage.expect({PLUGIN_PROTOCOL})
method = rd_string(msg)
if "TESTPLUGIN_PROTO_REJECT" in os.environ:
# Test the plugin failing at PLUGIN_PROTOCOL time.
msg = OutMessage(PLUGIN_PROTOCOL_REJECT)
wr_string_utf8(msg, os.environ["TESTPLUGIN_PROTO_REJECT"])
msg.send()
continue
# We only support keyboard-interactive. If we supported other
# auth methods, this would be the place to add further clauses
# to this if statement for them.
if method == b"keyboard-interactive":
msg = OutMessage(PLUGIN_PROTOCOL_ACCEPT)
msg.send()
# Inner loop run once per keyboard-interactive exchange
# with the SSH server.
while True:
# Expect a set of prompts from the server, or
# terminate the loop on SUCCESS or FAILURE.
#
# (We could also respond to SUCCESS or FAILURE by
# updating caches of our own, if we had any that were
# useful.)
msg = InMessage.expect({PLUGIN_KI_SERVER_REQUEST,
PLUGIN_AUTH_SUCCESS,
PLUGIN_AUTH_FAILURE})
if (msg.msgtype == PLUGIN_AUTH_SUCCESS or
msg.msgtype == PLUGIN_AUTH_FAILURE):
break
# If we didn't just break, we're sitting on a
# PLUGIN_KI_SERVER_REQUEST message. Get all its bits
# and pieces out.
name = rd_string_utf8(msg)
instructions = rd_string_utf8(msg)
language = rd_string(msg)
nprompts = rd_uint32(msg)
prompts = []
for i in range(nprompts):
prompt = rd_string_utf8(msg)
echo = rd_boolean(msg)
prompts.append((prompt, echo))
# Actually make up some answers for the prompts. This
# is the part that a non-example implementation would
# do very differently, of course!
#
# Here, we answer "foo" to every prompt, except that
# if there are exactly two prompts in the packet then
# we answer "stoat" to the first and "weasel" to the
# second.
#
# (These answers are consistent with the ones required
# by PuTTY's test SSH server Uppity in its own
# keyboard-interactive test implementation: that
# presents a two-prompt packet and expects
# "stoat","weasel" as the answers, and then presents a
# zero-prompt packet. So this test plugin will get you
# through Uppity's k-i in a one-touch manner. The
# "foo" in this code isn't used by Uppity at all; I
# just include it because I had to have _some_
# handling for the else clause.)
#
# If TESTPLUGIN_PROMPTS is set in the environment, we
# ask the user questions of our own by sending them
# back to PuTTY as USER_REQUEST messages.
if nprompts == 2:
if "TESTPLUGIN_PROMPTS" in os.environ:
for i in range(2):
# Make up some questions to ask.
msg = OutMessage(PLUGIN_KI_USER_REQUEST)
wr_string_utf8(
msg, "Plugin request #{:d} (name)".format(i))
wr_string_utf8(
msg, "Plugin request #{:d} (instructions)"
.format(i))
wr_string(msg, b"")
wr_uint32(msg, 2)
wr_string_utf8(msg, "Prompt 1 of 2 (echo): ")
wr_boolean(msg, True)
wr_string_utf8(msg, "Prompt 2 of 2 (no echo): ")
wr_boolean(msg, False)
msg.send()
# Expect the answers.
msg = InMessage.expect({PLUGIN_KI_USER_RESPONSE})
user_nprompts = rd_uint32(msg)
assert user_nprompts == 2, (
"Should match what we just sent")
for i in range(nprompts):
user_response = rd_string_utf8(msg)
# We don't actually check these
# responses for anything.
answers = ["stoat", "weasel"]
else:
answers = ["foo"] * nprompts
# Send the answers to the SSH server's questions.
msg = OutMessage(PLUGIN_KI_SERVER_RESPONSE)
wr_uint32(msg, len(answers))
for answer in answers:
wr_string_utf8(msg, answer)
msg.send()
else:
# Default handler if we don't speak the offered protocol
# at all.
msg = OutMessage(PLUGIN_PROTOCOL_REJECT)
wr_string_utf8(msg, "")
msg.send()
# Demonstration write to stderr, to prove that it shows up in PuTTY's
# Event Log.
print("Hello from test plugin's stderr", file=sys.stderr)
try:
protocol()
except PluginEOF:
pass

View File

@ -81,6 +81,7 @@ if(HALIBUT AND PERL_EXECUTABLE)
${CMAKE_CURRENT_SOURCE_DIR}/udp.but
${CMAKE_CURRENT_SOURCE_DIR}/pgpkeys.but
${CMAKE_CURRENT_SOURCE_DIR}/sshnames.but
${CMAKE_CURRENT_SOURCE_DIR}/authplugin.but
${CMAKE_CURRENT_SOURCE_DIR}/index.but
${VERSION_BUT})

519
doc/authplugin.but Normal file
View File

@ -0,0 +1,519 @@
\A{authplugin} PuTTY authentication plugin protocol
This appendix contains the specification for the protocol spoken over
local IPC between PuTTY and an authentication helper plugin.
If you already have an authentication plugin and want to configure
PuTTY to use it, see \k{config-ssh-authplugin} for how to do that.
This appendix is for people writing new authentication plugins.
\H{authplugin-req} Requirements
The following requirements informed the specification of this protocol.
\s{Automate keyboard-interactive authentication.} We're motivated in
the first place by the observation that the general SSH userauth
method \cq{keyboard-interactive} (defined in \k{authplugin-ref-ki})
can be used for many kinds of challenge/response or one-time-password
styles of authentication, and in more than one of those, the necessary
responses might be obtained from an auxiliary network connection, such
as an HTTPS transaction. So it's useful if a user doesn't have to
manually copy-type or copy-paste from their web browser into their SSH
client, but instead, the process can be automated.
\s{Be able to pass prompts on to the user.} On the other hand, some
userauth methods can be only \e{partially} automated; some of the
server's prompts might still require human input. Also, the plugin
automating the authentication might need to ask its own questions that
are not provided by the SSH server. (For example, \q{please enter the
master key that the real response will be generated by hashing}.) So
after the plugin intercepts the server's questions, it needs to be
able to ask its own questions of the user, which may or may not be the
same questions sent by the server.
\s{Allow automatic generation of the username.} Sometimes, the
authentication method comes with a mechanism for discovering the
username to be used in the SSH login. So the plugin has to start up
early enough that the client hasn't committed to a username yet.
\s{Future expansion route to other SSH userauth flavours.} The initial
motivation for this protocol is specific to keyboard-interactive. But
other SSH authentication methods exist, and they may also benefit from
automation in future. We're making no attempt here to predict what
those methods might be or how they might be automated, but we do need
to leave a space where they can be slotted in later if necessary.
\s{Minimal information loss.} Keyboard-interactive prompts and replies
should be passed to and from the plugin in a form as close as possible
to the way they look on the wire in SSH itself. Therefore, the
protocol resembles SSH in its data formats and marshalling (instead
of, for example, translating from SSH binary packet style to another
well-known format such as JSON, which would introduce edge cases in
character encoding).
\s{Half-duplex.} Simultaneously trying to read one I/O stream and
write another adds a lot of complexity to software. It becomes
necessary to have an organised event loop containing \cw{select} or
\cw{WaitForMultipleObjects} or similar, which can invoke the handler
for whichever event happens soonest. There's no need to add that
complexity in an application like this, which isn't transferring large
amounts of bulk data or multiplexing unrelated activities. So, to keep
life simple for plugin authors, we set the ground rule that it must
always be 100% clear which side is supposed to be sending a message
next. That way, the plugin can be written as sequential code
progressing through the protocol, making simple read and write calls
to receive or send each message.
\s{Communicate success/failure, to facilitate caching in the plugin.}
A plugin might want to cache recently used data for next time, but
only in the case where authentication using that data was actually
successful. So the client has to tell the plugin what the outcome was,
if it's known. (But this is best-effort only. Obviously the plugin
cannot \e{depend} on hearing the answer, because any IPC protocol at
all carries the risk that the other end might crash or be killed by
things outside its control.)
\H{authplugin-transport} Transport and configuration
Plugins are executable programs on the client platform.
The SSH client must be manually configured to use a plugin for a
particular connection. The configuration takes the form of a command
line, including the location of the plugin executable, and optionally
command-line arguments that are meaningful to the particular plugin.
The client invokes the plugin as a subprocess, passing it a pair of
8-bit-clean pipes as its standard input and output. On those pipes,
the client and plugin will communicate via the protocol specified
below.
\H{authplugin-formats} Data formats and marshalling
This protocol borrows the low-level data formatting from SSH itself,
in particular the following wire encodings from
\k{authplugin-ref-arch} section 5:
\dt \s{byte}
\dd An integer between 0 and 0xFF inclusive, transmitted as a single
byte of binary data.
\dt \s{boolean}
\dd The values \q{true} or \q{false}, transmitted as the bytes 1 and 0
respectively.
\dt \s{uint32}
\dd An integer between 0 and 0xFFFFFFFF inclusive, transmitted as 4
bytes of binary data, in big-endian (\q{network}) byte order.
\dt \s{string}
\dd A sequence of bytes, preceded by a \s{uint32} giving the number of
bytes in the sequence. The length field does not include itself. For
example, the empty string is represented by four zero bytes (the
\s{uint32} encoding of 0); the string "AB" is represented by the six
bytes 0,0,0,2,'A','B'.
Unlike SSH itself, the protocol spoken between the client and the
plugin is unencrypted, because local inter-process pipes are assumed
to be secured by the OS kernel. So the binary packet protocol is much
simpler than SSH proper, and is similar to SFTP and the OpenSSH agent
protocol.
The data sent in each direction of the conversation consists of a
sequence of \s{messages} exchanged between the SSH client and the
plugin. Each message is encoded as a \s{string}. The contents of the
string begin with a \s{byte} giving the message type, which determines
the format of the rest of the message.
\H{authplugin-version} Protocol versioning
This protocol itself is versioned. At connection setup, the client
states the highest version number it knows how to speak, and then the
plugin responds by choosing the version number that will actually be
spoken (which may not be higher than the client's value).
Including a version number makes it possible to make breaking changes
to the protocol later.
Even version numbers represent released versions of this spec. Odd
numbers represent drafts or development versions in between releases.
A client and plugin negotiating an odd version number are not
guaranteed to interoperate; the developer testing the combination is
responsible for ensuring the two are compatible.
This document describes version 2 of the protocol, the first released
version. (The initial drafts had version 1.)
\H{authplugin-overview} Overview and sequence of events
At the very beginning of the user authentication phase of SSH, the
client launches the plugin subprocess, if one is configured. It
immediately sends the \cw{PLUGIN_INIT} message, telling the plugin
some initial information about where the SSH connection is to.
The plugin responds with \cw{PLUGIN_INIT_RESPONSE}, which may
optionally tell the SSH client what username to use.
The client begins trying to authenticate with the SSH server in the
usual way, using the username provided by the plugin (if any) or
alternatively one obtained via its normal (non-plugin) policy.
The client follows its normal policy for selecting authentication
methods to attempt. If it chooses a method that this protocol does not
cover, then the client will perform that method in its own way without
consulting the plugin.
However, if the client and server decide to attempt a method that this
protocol \e{does} cover, then the client sends \cw{PLUGIN_PROTOCOL}
specifying the SSH protocol id for the authentication method being
used. The plugin responds with \cw{PLUGIN_PROTOCOL_ACCEPT} if it's
willing to assist with this auth method, or
\cw{PLUGIN_PROTOCOL_REJECT} if it isn't.
If the plugin sends \cw{PLUGIN_PROTOCOL_REJECT}, then the client will
proceed as if the plugin were not present. Later, if another auth
method is negotiated (either because this one failed, or because it
succeeded but the server wants multiple auth methods), the client may
send a further \cw{PLUGIN_PROTOCOL} and try again.
If the plugin sends \cw{PLUGIN_PROTOCOL_ACCEPT}, then a protocol
segment begins that is specific to that auth method, terminating in
either \cw{PLUGIN_AUTH_SUCCESS} or \cw{PLUGIN_AUTH_FAILURE}. After
that, again, the client may send a further \cw{PLUGIN_PROTOCOL}.
Currently the only supported method is \cq{keyboard-interactive},
defined in \k{authplugin-ref-ki}. Once the client has announced this
to the server, the followup protocol is as follows:
Each time the server sends an \cw{SSH_MSG_USERAUTH_INFO_REQUEST}
message requesting authentication responses from the user, the SSH
client translates the message into \cw{PLUGIN_KI_SERVER_REQUEST} and
passes it on to the plugin.
At this point, the plugin may optionally send back
\cw{PLUGIN_KI_USER_REQUEST} containing prompts to be presented to the
actual user. The client will reply with a matching
\cw{PLUGIN_KI_USER_RESPONSE} after asking the user to reply to the
question(s) in the request message. The plugin can repeat this cycle
multiple times.
Once the plugin has all the information it needs to respond to the
server's authentication prompts, it sends \cw{PLUGIN_KI_SERVER_RESPONSE}
back to the client, which translates it into
\cw{SSH_MSG_USERAUTH_INFO_RESPONSE} to send on to the server.
After that, as described in \k{authplugin-ref-ki}, the server is free
to accept authentication, reject it, or send another
\cw{SSH_MSG_USERAUTH_INFO_REQUEST}. Each
\cw{SSH_MSG_USERAUTH_INFO_REQUEST} is dealt with in the same way as
above.
If the server terminates keyboard-interactive authentication with
\cw{SSH_MSG_USERAUTH_SUCCESS} or \cw{SSH_MSG_USERAUTH_FAILURE}, the
client informs the plugin by sending either \cw{PLUGIN_AUTH_SUCCESS}
or \cw{PLUGIN_AUTH_FAILURE}. \cw{PLUGIN_AUTH_SUCCESS} is sent when
\e{that particular authentication method} was successful, regardless
of whether the SSH server chooses to request further authentication
afterwards: in particular, \cw{SSH_MSG_USERAUTH_FAILURE} with the
\q{partial success} flag (see \k{authplugin-ref-userauth} section 5.1) translates
into \cw{PLUGIN_AUTH_SUCCESS}.
The plugin's standard input will close when the client no longer
requires the plugin's services, for any reason. This could be because
authentication is complete (with overall success or overall failure),
or because the user has manually aborted the session in
mid-authentication, or because the client crashed.
\H{authplugin-messages} Message formats
This section describes the format of every message in the protocol.
As described in \k{authplugin-formats}, every message starts with the same two
fields:
\b \s{uint32}: overall length of the message
\b \s{byte}: message type.
The length field does not include itself, but does include the type
code.
The following subsections each give the format of the remainder of the
message, after the type code.
The type codes themselves are defined here:
\c #define PLUGIN_INIT 1
\c #define PLUGIN_INIT_RESPONSE 2
\c #define PLUGIN_PROTOCOL 3
\c #define PLUGIN_PROTOCOL_ACCEPT 4
\c #define PLUGIN_PROTOCOL_REJECT 5
\c #define PLUGIN_AUTH_SUCCESS 6
\c #define PLUGIN_AUTH_FAILURE 7
\c #define PLUGIN_INIT_FAILURE 8
\c
\c #define PLUGIN_KI_SERVER_REQUEST 20
\c #define PLUGIN_KI_SERVER_RESPONSE 21
\c #define PLUGIN_KI_USER_REQUEST 22
\c #define PLUGIN_KI_USER_RESPONSE 23
If this protocol is extended to be able to assist with further auth
methods, their message type codes will also begin from 20, overlapping
the codes for keyboard-interactive.
\S{PLUGIN_INIT} \cw{PLUGIN_INIT}
\s{Direction}: client to plugin
\s{When}: the first message sent at connection startup
\s{What happens next}: the plugin will send \cw{PLUGIN_INIT_RESPONSE}
or \cw{PLUGIN_INIT_FAILURE}
\s{Message contents after the type code}:
\b \s{uint32}: the highest version number of this protocol that the
client knows how to speak.
\b \s{string}: the hostname of the server. This will be the \e{logical}
hostname, in cases where it differs from the physical destination of
the network connection. Whatever name would be used by the SSH client
to cache the server's host key, that's the same name passed in this
message.
\b \s{uint32}: the port number on the server. (Together with the host
name, this forms a primary key identifying a particular server. Port
numbers may be vital because a single host can run two unrelated SSH
servers with completely different authentication requirements, e.g.
system sshd on port 22 and Gerrit on port 29418.)
\b \s{string}: the username that the client will use to log in, if the
plugin chooses not to override it. An empty string means that the
client has no opinion about this (and might, for example, prompt the
user).
\S{PLUGIN_INIT_RESPONSE} \cw{PLUGIN_INIT_RESPONSE}
\s{Direction}: plugin to client
\s{When}: response to \cw{PLUGIN_INIT}
\s{What happens next}: the client will send \cw{PLUGIN_PROTOCOL}, or
perhaps terminate the session (if no auth method is ever negotiated
that the plugin can help with)
\s{Message contents after the type code}:
\b \s{uint32}: the version number of this protocol that the connection
will use. Must be no greater than the max version number sent by the
client in \cw{PLUGIN_INIT}.
\b \s{string}: the username that the plugin suggests the client use. An
empty string means that the plugin has no opinion and the client
should stick with the username it already had (or prompt the user, if
it had none).
\S{PLUGIN_INIT_FAILURE} \cw{PLUGIN_INIT_FAILURE}
\s{Direction}: plugin to client
\s{When}: response to \cw{PLUGIN_INIT}
\s{What happens next}: the session is over
\s{Message contents after the type code}:
\b \s{string}: an error message to present to the user indicating why
the plugin was unable to start up.
\S{PLUGIN_PROTOCOL} \cw{PLUGIN_PROTOCOL}
\s{Direction}: client to plugin
\s{When}: sent after \cw{PLUGIN_INIT_RESPONSE}, or after a previous
auth phase terminates with \cw{PLUGIN_AUTH_SUCCESS} or
\cw{PLUGIN_AUTH_FAILURE}
\s{What happens next}: the plugin will send
\cw{PLUGIN_PROTOCOL_ACCEPT} or \cw{PLUGIN_PROTOCOL_REJECT}
\s{Message contents after the type code}:
\b \s{string}: the SSH protocol id of the auth method the client
intends to attempt. Currently the only method specified for use in
this protocol is \cq{keyboard-interactive}.
\S{PLUGIN_PROTOCOL_REJECT} \cw{PLUGIN_PROTOCOL_REJECT}
\s{Direction}: plugin to client
\s{When}: sent after \cw{PLUGIN_PROTOCOL}
\s{What happens next}: the client will either send another
\cw{PLUGIN_PROTOCOL} or terminate the session
\s{Message contents after the type code}:
\b \s{string}: an error message to present to the user, explaining why
the plugin cannot help with this authentication protocol.
\lcont{
An example might be \q{unable to open <config file>: <OS error
message>}, if the plugin depends on some configuration that the user
has not set up.
If the plugin does not support this this particular authentication
protocol at all, this string should be left blank, so that no message
will be presented to the user at all.
}
\S{PLUGIN_PROTOCOL_ACCEPT} \cw{PLUGIN_PROTOCOL_ACCEPT}
\s{Direction}: plugin to client
\s{When}: sent after \cw{PLUGIN_PROTOCOL}
\s{What happens next}: depends on the auth protocol agreed on. For
keyboard-interactive, the client will send
\cw{PLUGIN_KI_SERVER_REQUEST} or \cw{PLUGIN_AUTH_SUCCESS} or
\cw{PLUGIN_AUTH_FAILURE}. No other method is specified.
\s{Message contents after the type code}: none.
\S{PLUGIN_KI_SERVER_REQUEST} \cw{PLUGIN_KI_SERVER_REQUEST}
\s{Direction}: client to plugin
\s{When}: sent after \cw{PLUGIN_PROTOCOL}, or after a previous
\cw{PLUGIN_KI_SERVER_RESPONSE}, when the SSH server has sent
\cw{SSH_MSG_USERAUTH_INFO_REQUEST}
\s{What happens next}: the plugin will send either
\cw{PLUGIN_KI_USER_REQUEST} or \cw{PLUGIN_KI_SERVER_RESPONSE}
\s{Message contents after the type code}: the exact contents of the
\cw{SSH_MSG_USERAUTH_INFO_REQUEST} just sent by the server. See
\k{authplugin-ref-ki} section 3.2 for details. The summary:
\b \s{string}: name of this prompt collection (e.g. to use as a
dialog-box title)
\b \s{string}: instructions to be displayed before this prompt
collection
\b \s{string}: language tag (deprecated)
\b \s{uint32}: number of prompts in this collection
\b That many copies of:
\lcont{
\b \s{string}: prompt (in UTF-8)
\b \s{boolean}: whether the response to this prompt is safe to echo to
the screen
}
\S{PLUGIN_KI_SERVER_RESPONSE} \cw{PLUGIN_KI_SERVER_RESPONSE}
\s{Direction}: plugin to client
\s{When}: response to \cw{PLUGIN_KI_SERVER_REQUEST}, perhaps after one
or more intervening pairs of \cw{PLUGIN_KI_USER_REQUEST} and
\cw{PLUGIN_KI_USER_RESPONSE}
\s{What happens next}: the client will send a further
\cw{PLUGIN_KI_SERVER_REQUEST}, or \cw{PLUGIN_AUTH_SUCCESS} or
\cw{PLUGIN_AUTH_FAILURE}
\s{Message contents after the type code}: the exact contents of the
\cw{SSH_MSG_USERAUTH_INFO_RESPONSE} that the client should send back
to the server. See \k{authplugin-ref-ki} section 3.4 for details. The
summary:
\b \s{uint32}: number of responses (must match the \q{number of
prompts} field from the corresponding server request)
\b That many copies of:
\lcont{
\b \s{string}: response to the \e{n}th prompt (in UTF-8)
}
\S{PLUGIN_KI_USER_REQUEST} \cw{PLUGIN_KI_USER_REQUEST}
\s{Direction}: plugin to client
\s{When}: response to \cw{PLUGIN_KI_SERVER_REQUEST}, if the plugin
cannot answer the server's auth prompts without presenting prompts of
its own to the user
\s{What happens next}: the client will send \cw{PLUGIN_KI_USER_RESPONSE}
\s{Message contents after the type code}: exactly the same as in
\cw{PLUGIN_KI_SERVER_REQUEST} (see \k{PLUGIN_KI_SERVER_REQUEST}).
\S{PLUGIN_KI_USER_RESPONSE} \cw{PLUGIN_KI_USER_RESPONSE}
\s{Direction}: client to plugin
\s{When}: response to \cw{PLUGIN_KI_USER_REQUEST}
\s{What happens next}: the plugin will send
\cw{PLUGIN_KI_SERVER_RESPONSE}, or another \cw{PLUGIN_KI_USER_REQUEST}
\s{Message contents after the type code}: exactly the same as in
\cw{PLUGIN_KI_SERVER_RESPONSE} (see \k{PLUGIN_KI_SERVER_RESPONSE}).
\S{PLUGIN_AUTH_SUCCESS} \cw{PLUGIN_AUTH_SUCCESS}
\s{Direction}: client to plugin
\s{When}: sent after \cw{PLUGIN_KI_SERVER_RESPONSE}, or (in unusual
cases) after \cw{PLUGIN_PROTOCOL_ACCEPT}
\s{What happens next}: the client will either send another
\cw{PLUGIN_PROTOCOL} or terminate the session
\s{Message contents after the type code}: none
\S{PLUGIN_AUTH_FAILURE} \cw{PLUGIN_AUTH_FAILURE}
\s{Direction}: client to plugin
\s{When}: sent after \cw{PLUGIN_KI_SERVER_RESPONSE}, or (in unusual
cases) after \cw{PLUGIN_PROTOCOL_ACCEPT}
\s{What happens next}: the client will either send another
\cw{PLUGIN_PROTOCOL} or terminate the session
\s{Message contents after the type code}: none
\H{authplugin-refs} References
\B{authplugin-ref-arch} \W{https://datatracker.ietf.org/doc/html/rfc4251}{RFC 4251}, \q{The Secure Shell (SSH) Protocol
Architecture}.
\B{authplugin-ref-userauth} \W{https://datatracker.ietf.org/doc/html/rfc4252}{RFC
4252}, \q{The Secure Shell (SSH) Authentication Protocol}.
\B{authplugin-ref-ki}
\W{https://datatracker.ietf.org/doc/html/rfc4256}{RFC 4256},
\q{Generic Message Exchange Authentication for the Secure Shell
Protocol (SSH)} (better known by its wire id
\q{keyboard-interactive}).
\BR{authplugin-ref-arch} [RFC4251]
\BR{authplugin-ref-userauth} [RFC4252]
\BR{authplugin-ref-ki} [RFC4256]

View File

@ -2965,6 +2965,12 @@ username more than once, in case the server complains. If you know
your server can cope with it, you can enable the \q{Allow attempted
changes of username} option to modify PuTTY's behaviour.
\H{config-ssh-auth-creds} The Credentials panel
This subpane of the Auth panel contains configuration options that
specify actual \e{credentials} to present to the server: key files and
certificates.
\S{config-ssh-privkey} \q{\ii{Private key} file for authentication}
This box is where you enter the name of your private key file if you
@ -3014,6 +3020,26 @@ To do this, enter the pathname of the certificate file into the
When this setting is configured, PuTTY will honour it no matter
whether the private key is found in a file, or loaded into Pageant.
\S{config-ssh-authplugin} \q{\ii{Plugin} to provide authentication responses}
An SSH server can use the \q{keyboard-interactive} protocol to present
a series of arbitrary questions and answers. Sometimes this is used
for ordinary passwords, but sometimes the server will use the same
mechanism for something more complicated, such as a one-time password
system.
Some of these systems can be automated. For this purpose, PuTTY allows
you to provide a separate program to act as a \q{plugin} which will
take over the authentication and send answers to the questions on your
behalf.
If you have been provided with a plugin of this type, you can
configure it here, by entering a full command line in the \q{Plugin
command to run} box.
(If you want to \e{write} a plugin of this type, see \k{authplugin}
for the full specification of how the plugin is expected to behave.)
\H{config-ssh-auth-gssapi} The \i{GSSAPI} panel
The \q{GSSAPI} subpanel of the \q{Auth} panel controls the use of

View File

@ -1831,6 +1831,7 @@ NORETURN void cleanup_exit(int);
X(INT, INT, ssh_cipherlist) \
X(FILENAME, NONE, keyfile) \
X(FILENAME, NONE, detached_cert) \
X(STR, NONE, auth_plugin) \
/* \
* Which SSH protocol to use. \
* For historical reasons, the current legal values for CONF_sshprot \

View File

@ -633,6 +633,7 @@ void save_open_settings(settings_w *sesskey, Conf *conf)
write_setting_b(sesskey, "SSH2DES", conf_get_bool(conf, CONF_ssh2_des_cbc));
write_setting_filename(sesskey, "PublicKeyFile", conf_get_filename(conf, CONF_keyfile));
write_setting_filename(sesskey, "DetachedCertificate", conf_get_filename(conf, CONF_detached_cert));
write_setting_s(sesskey, "AuthPlugin", conf_get_str(conf, CONF_auth_plugin));
write_setting_s(sesskey, "RemoteCommand", conf_get_str(conf, CONF_remote_cmd));
write_setting_b(sesskey, "RFCEnviron", conf_get_bool(conf, CONF_rfc_environ));
write_setting_b(sesskey, "PassiveTelnet", conf_get_bool(conf, CONF_passive_telnet));
@ -1052,6 +1053,7 @@ void load_open_settings(settings_r *sesskey, Conf *conf)
gppb(sesskey, "SshNoShell", false, conf, CONF_ssh_no_shell);
gppfile(sesskey, "PublicKeyFile", conf, CONF_keyfile);
gppfile(sesskey, "DetachedCertificate", conf, CONF_detached_cert);
gpps(sesskey, "AuthPlugin", "", conf, CONF_auth_plugin);
gpps(sesskey, "RemoteCommand", "", conf, CONF_remote_cmd);
gppb(sesskey, "RFCEnviron", false, conf, CONF_rfc_environ);
gppb(sesskey, "PassiveTelnet", false, conf, CONF_passive_telnet);

32
ssh.h
View File

@ -1917,3 +1917,35 @@ bool ssh_transient_hostkey_cache_verify(
bool ssh_transient_hostkey_cache_has(
ssh_transient_hostkey_cache *thc, const ssh_keyalg *alg);
bool ssh_transient_hostkey_cache_non_empty(ssh_transient_hostkey_cache *thc);
/*
* Protocol definitions for authentication helper plugins
*/
#define AUTHPLUGIN_MSG_NAMES(X) \
X(PLUGIN_INIT, 1) \
X(PLUGIN_INIT_RESPONSE, 2) \
X(PLUGIN_PROTOCOL, 3) \
X(PLUGIN_PROTOCOL_ACCEPT, 4) \
X(PLUGIN_PROTOCOL_REJECT, 5) \
X(PLUGIN_AUTH_SUCCESS, 6) \
X(PLUGIN_AUTH_FAILURE, 7) \
X(PLUGIN_INIT_FAILURE, 8) \
X(PLUGIN_KI_SERVER_REQUEST, 20) \
X(PLUGIN_KI_SERVER_RESPONSE, 21) \
X(PLUGIN_KI_USER_REQUEST, 22) \
X(PLUGIN_KI_USER_RESPONSE, 23) \
/* end of list */
#define PLUGIN_PROTOCOL_MAX_VERSION 2 /* the highest version we speak */
enum {
#define ENUMDECL(name, value) name = value,
AUTHPLUGIN_MSG_NAMES(ENUMDECL)
#undef ENUMDECL
/* Error codes internal to this implementation, indicating failure
* to receive a meaningful packet at all */
PLUGIN_NOTYPE = 256, /* packet too short to have a type */
PLUGIN_EOF = 257 /* EOF from auth plugin */
};

View File

@ -114,7 +114,8 @@ PacketProtocolLayer *ssh2_userauth_new(
bool show_banner, bool tryagent, bool notrivialauth,
const char *default_username, bool change_username,
bool try_ki_auth, bool try_gssapi_auth, bool try_gssapi_kex_auth,
bool gssapi_fwd, struct ssh_connection_shared_gss_state *shgss);
bool gssapi_fwd, struct ssh_connection_shared_gss_state *shgss,
const char *auth_plugin);
PacketProtocolLayer *ssh2_connection_new(
Ssh *ssh, ssh_sharing_state *connshare, bool is_simple,
Conf *conf, const char *peer_verstring, bufchain *user_input,

View File

@ -267,14 +267,14 @@ static void ssh_got_ssh_version(struct ssh_version_receiver *rcv,
conf_get_bool(ssh->conf, CONF_try_gssapi_auth),
conf_get_bool(ssh->conf, CONF_try_gssapi_kex),
conf_get_bool(ssh->conf, CONF_gssapifwd),
&ssh->gss_state
&ssh->gss_state,
#else
false,
false,
false,
NULL
NULL,
#endif
);
conf_get_str(ssh->conf, CONF_auth_plugin));
ssh_connect_ppl(ssh, userauth_layer);
transport_child_layer = userauth_layer;

View File

@ -90,6 +90,15 @@ struct ssh2_userauth_state {
StripCtrlChars *banner_scc;
bool banner_scc_initialised;
char *authplugin_cmd;
Socket *authplugin;
uint32_t authplugin_version;
Plug authplugin_plug;
bufchain authplugin_bc;
strbuf *authplugin_incoming_msg;
bool authplugin_eof;
bool authplugin_ki_active;
StripCtrlChars *ki_scc;
bool ki_scc_initialised;
bool ki_printed_header;
@ -118,7 +127,7 @@ static PktOut *ssh2_userauth_gss_packet(
struct ssh2_userauth_state *s, const char *authtype);
#endif
static bool ssh2_userauth_ki_setup_prompts(
struct ssh2_userauth_state *s, BinarySource *src);
struct ssh2_userauth_state *s, BinarySource *src, bool plugin);
static bool ssh2_userauth_ki_run_prompts(struct ssh2_userauth_state *s);
static void ssh2_userauth_ki_write_responses(
struct ssh2_userauth_state *s, BinarySink *bs);
@ -140,7 +149,8 @@ PacketProtocolLayer *ssh2_userauth_new(
bool show_banner, bool tryagent, bool notrivialauth,
const char *default_username, bool change_username,
bool try_ki_auth, bool try_gssapi_auth, bool try_gssapi_kex_auth,
bool gssapi_fwd, struct ssh_connection_shared_gss_state *shgss)
bool gssapi_fwd, struct ssh_connection_shared_gss_state *shgss,
const char *authplugin_cmd)
{
struct ssh2_userauth_state *s = snew(struct ssh2_userauth_state);
memset(s, 0, sizeof(*s));
@ -166,6 +176,8 @@ PacketProtocolLayer *ssh2_userauth_new(
s->is_trivial_auth = true;
bufchain_init(&s->banner);
bufchain_sink_init(&s->banner_bs, &s->banner);
s->authplugin_cmd = dupstr(authplugin_cmd);
bufchain_init(&s->authplugin_bc);
return &s->ppl;
}
@ -218,6 +230,12 @@ static void ssh2_userauth_free(PacketProtocolLayer *ppl)
stripctrl_free(s->banner_scc);
if (s->ki_scc)
stripctrl_free(s->ki_scc);
sfree(s->authplugin_cmd);
if (s->authplugin)
sk_close(s->authplugin);
bufchain_clear(&s->authplugin_bc);
if (s->authplugin_incoming_msg)
strbuf_free(s->authplugin_incoming_msg);
sfree(s);
}
@ -288,6 +306,125 @@ static bool ssh2_userauth_signflags(struct ssh2_userauth_state *s,
return true;
}
static void authplugin_plug_log(Plug *plug, PlugLogType type, SockAddr *addr,
int port, const char *err_msg, int err_code)
{
struct ssh2_userauth_state *s = container_of(
plug, struct ssh2_userauth_state, authplugin_plug);
PacketProtocolLayer *ppl = &s->ppl; /* for ppl_logevent */
if (type == PLUGLOG_PROXY_MSG)
ppl_logevent("%s", err_msg);
}
static void authplugin_plug_closing(
Plug *plug, PlugCloseType type, const char *error_msg)
{
struct ssh2_userauth_state *s = container_of(
plug, struct ssh2_userauth_state, authplugin_plug);
s->authplugin_eof = true;
queue_idempotent_callback(&s->ppl.ic_process_queue);
}
static void authplugin_plug_receive(
Plug *plug, int urgent, const char *data, size_t len)
{
struct ssh2_userauth_state *s = container_of(
plug, struct ssh2_userauth_state, authplugin_plug);
bufchain_add(&s->authplugin_bc, data, len);
queue_idempotent_callback(&s->ppl.ic_process_queue);
}
static const PlugVtable authplugin_plugvt = {
.log = authplugin_plug_log,
.closing = authplugin_plug_closing,
.receive = authplugin_plug_receive,
.sent = nullplug_sent,
};
static strbuf *authplugin_newmsg(uint8_t type)
{
strbuf *amsg = strbuf_new_nm();
put_uint32(amsg, 0); /* fill in later */
put_byte(amsg, type);
return amsg;
}
static void authplugin_send_free(struct ssh2_userauth_state *s, strbuf *amsg)
{
PUT_32BIT_MSB_FIRST(amsg->u, amsg->len - 4);
assert(s->authplugin);
sk_write(s->authplugin, amsg->u, amsg->len);
strbuf_free(amsg);
}
static bool authplugin_expect_msg(struct ssh2_userauth_state *s,
unsigned *type, BinarySource *src)
{
if (s->authplugin_eof) {
*type = PLUGIN_EOF;
return true;
}
uint8_t len[4];
if (!bufchain_try_fetch(&s->authplugin_bc, len, 4))
return false;
size_t size = GET_32BIT_MSB_FIRST(len);
if (bufchain_size(&s->authplugin_bc) - 4 < size)
return false;
if (s->authplugin_incoming_msg) {
strbuf_clear(s->authplugin_incoming_msg);
} else {
s->authplugin_incoming_msg = strbuf_new_nm();
}
bufchain_consume(&s->authplugin_bc, 4); /* eat length field */
bufchain_fetch_consume(
&s->authplugin_bc, strbuf_append(s->authplugin_incoming_msg, size),
size);
BinarySource_BARE_INIT_PL(
src, ptrlen_from_strbuf(s->authplugin_incoming_msg));
*type = get_byte(src);
if (get_err(src))
*type = PLUGIN_NOTYPE;
return true;
}
static void authplugin_bad_packet(struct ssh2_userauth_state *s,
unsigned type, const char *fmt, ...)
{
strbuf *msg = strbuf_new();
switch (type) {
case PLUGIN_EOF:
put_dataz(msg, "Unexpected end of file from auth helper plugin");
break;
case PLUGIN_NOTYPE:
put_dataz(msg, "Received malformed packet from auth helper plugin "
"(too short to have a type code)");
break;
default:
put_fmt(msg, "Received unknown message type %u "
"from auth helper plugin", type);
break;
#define CASEDECL(name, value) \
case name: \
put_fmt(msg, "Received unexpected %s message from auth helper " \
"plugin", #name); \
break;
AUTHPLUGIN_MSG_NAMES(CASEDECL);
#undef CASEDECL
}
if (fmt) {
put_dataz(msg, " (");
va_list ap;
va_start(ap, fmt);
put_fmt(msg, fmt, ap);
va_end(ap);
put_dataz(msg, ")");
}
ssh_sw_abort(s->ppl.ssh, "%s", msg->s);
strbuf_free(msg);
}
static void ssh2_userauth_process_queue(PacketProtocolLayer *ppl)
{
struct ssh2_userauth_state *s =
@ -502,6 +639,74 @@ static void ssh2_userauth_process_queue(PacketProtocolLayer *ppl)
done_agent_query:;
}
s->got_username = false;
if (*s->authplugin_cmd) {
s->authplugin_plug.vt = &authplugin_plugvt;
s->authplugin = platform_start_subprocess(
s->authplugin_cmd, &s->authplugin_plug, "plugin");
ppl_logevent("Started authentication plugin: %s", s->authplugin_cmd);
}
if (s->authplugin) {
strbuf *amsg = authplugin_newmsg(PLUGIN_INIT);
put_uint32(amsg, PLUGIN_PROTOCOL_MAX_VERSION);
put_stringz(amsg, s->hostname);
put_uint32(amsg, s->port);
put_stringz(amsg, s->username ? s->username : "");
authplugin_send_free(s, amsg);
BinarySource src[1];
unsigned type;
crMaybeWaitUntilV(authplugin_expect_msg(s, &type, src));
switch (type) {
case PLUGIN_INIT_RESPONSE: {
s->authplugin_version = get_uint32(src);
ptrlen username = get_string(src);
if (get_err(src)) {
ssh_sw_abort(s->ppl.ssh, "Received malformed "
"PLUGIN_INIT_RESPONSE from auth helper plugin");
return;
}
if (s->authplugin_version > PLUGIN_PROTOCOL_MAX_VERSION) {
ssh_sw_abort(s->ppl.ssh, "Auth helper plugin announced "
"unsupported version number %"PRIu32,
s->authplugin_version);
return;
}
if (username.len) {
sfree(s->default_username);
s->default_username = mkstr(username);
ppl_logevent("Authentication plugin set username '%s'",
s->default_username);
}
break;
}
case PLUGIN_INIT_FAILURE: {
ptrlen message = get_string(src);
if (get_err(src)) {
ssh_sw_abort(s->ppl.ssh, "Received malformed "
"PLUGIN_INIT_FAILURE from auth helper plugin");
return;
}
/* This is a controlled error, so we need not completely
* abandon the connection. Instead, inform the user, and
* proceed as if the plugin was not present */
ppl_printf("Authentication plugin failed to initialise:\r\n");
seat_set_trust_status(s->ppl.seat, false);
ppl_printf("%.*s\r\n", PTRLEN_PRINTF(message));
seat_set_trust_status(s->ppl.seat, true);
sk_close(s->authplugin);
s->authplugin = NULL;
break;
}
default:
authplugin_bad_packet(s, type, "expected PLUGIN_INIT_RESPONSE or "
"PLUGIN_INIT_FAILURE");
return;
}
}
/*
* We repeat this whole loop, including the username prompt,
* until we manage a successful authentication. If the user
@ -526,7 +731,6 @@ static void ssh2_userauth_process_queue(PacketProtocolLayer *ppl)
* the username they will want to be able to get back and
* retype it!
*/
s->got_username = false;
while (1) {
/*
* Get a username.
@ -1341,6 +1545,64 @@ static void ssh2_userauth_process_queue(PacketProtocolLayer *ppl)
ppl_logevent("Attempting keyboard-interactive authentication");
if (s->authplugin) {
strbuf *amsg = authplugin_newmsg(PLUGIN_PROTOCOL);
put_stringz(amsg, "keyboard-interactive");
authplugin_send_free(s, amsg);
BinarySource src[1];
unsigned type;
crMaybeWaitUntilV(authplugin_expect_msg(s, &type, src));
switch (type) {
case PLUGIN_PROTOCOL_REJECT: {
ptrlen message = PTRLEN_LITERAL("");
if (s->authplugin_version >= 2) {
/* draft protocol didn't include a message here */
message = get_string(src);
}
if (get_err(src)) {
ssh_sw_abort(s->ppl.ssh, "Received malformed "
"PLUGIN_PROTOCOL_REJECT from auth "
"helper plugin");
return;
}
if (message.len) {
/* If the plugin sent a message about
* _why_ it didn't want to do k-i, pass
* that message on to the user. (It might
* say, for example, what went wrong when
* it tried to open its config file.) */
ppl_printf("Authentication plugin failed to set "
"up keyboard-interactive "
"authentication:\r\n");
seat_set_trust_status(s->ppl.seat, false);
ppl_printf("%.*s\r\n", PTRLEN_PRINTF(message));
seat_set_trust_status(s->ppl.seat, true);
ppl_logevent("Authentication plugin declined to "
"help with keyboard-interactive: "
"%.*s", PTRLEN_PRINTF(message));
} else {
ppl_logevent("Authentication plugin declined to "
"help with keyboard-interactive");
}
s->authplugin_ki_active = false;
break;
}
case PLUGIN_PROTOCOL_ACCEPT:
s->authplugin_ki_active = true;
ppl_logevent("Authentication plugin agreed to help "
"with keyboard-interactive");
break;
default:
authplugin_bad_packet(
s, type, "expected PLUGIN_PROTOCOL_ACCEPT or "
"PLUGIN_PROTOCOL_REJECT");
return;
}
} else {
s->authplugin_ki_active = false;
}
if (!s->ki_scc_initialised) {
s->ki_scc = seat_stripctrl_new(
s->ppl.seat, NULL, SIC_KI_PROMPTS);
@ -1364,44 +1626,123 @@ static void ssh2_userauth_process_queue(PacketProtocolLayer *ppl)
s->ki_printed_header = false;
/*
* Loop while the server continues to send INFO_REQUESTs.
* Loop while we still have prompts to send to the user.
*/
while (pktin->type == SSH2_MSG_USERAUTH_INFO_REQUEST) {
if (!ssh2_userauth_ki_setup_prompts(
s, BinarySource_UPCAST(pktin)))
return;
crMaybeWaitUntilV(ssh2_userauth_ki_run_prompts(s));
if (!s->authplugin_ki_active) {
/*
* The simple case: INFO_REQUESTs are passed on to
* the user, and responses are sent straight back
* to the SSH server.
*/
while (pktin->type == SSH2_MSG_USERAUTH_INFO_REQUEST) {
if (!ssh2_userauth_ki_setup_prompts(
s, BinarySource_UPCAST(pktin), false))
return;
crMaybeWaitUntilV(ssh2_userauth_ki_run_prompts(s));
if (spr_is_abort(s->spr)) {
/*
* Failed to get responses. Terminate.
*/
free_prompts(s->cur_prompt);
s->cur_prompt = NULL;
ssh_bpp_queue_disconnect(
s->ppl.bpp, "Unable to authenticate",
SSH2_DISCONNECT_AUTH_CANCELLED_BY_USER);
ssh_spr_close(s->ppl.ssh, s->spr, "keyboard-"
"interactive authentication prompt");
return;
}
if (spr_is_abort(s->spr)) {
/*
* Failed to get responses. Terminate.
* Send the response(s) to the server.
*/
free_prompts(s->cur_prompt);
s->cur_prompt = NULL;
ssh_bpp_queue_disconnect(
s->ppl.bpp, "Unable to authenticate",
SSH2_DISCONNECT_AUTH_CANCELLED_BY_USER);
ssh_spr_close(s->ppl.ssh, s->spr, "keyboard-"
"interactive authentication prompt");
return;
s->pktout = ssh_bpp_new_pktout(
s->ppl.bpp, SSH2_MSG_USERAUTH_INFO_RESPONSE);
ssh2_userauth_ki_write_responses(
s, BinarySink_UPCAST(s->pktout));
s->pktout->minlen = 256;
pq_push(s->ppl.out_pq, s->pktout);
/*
* Get the next packet in case it's another
* INFO_REQUEST.
*/
crMaybeWaitUntilV(
(pktin = ssh2_userauth_pop(s)) != NULL);
}
} else {
/*
* Send the response(s) to the server.
* The case where a plugin is involved:
* INFO_REQUEST from the server is sent to the
* plugin, which sends responses that we hand back
* to the server. But in the meantime, the plugin
* might send USER_REQUEST for us to pass to the
* user, and then we send responses to that.
*/
s->pktout = ssh_bpp_new_pktout(
s->ppl.bpp, SSH2_MSG_USERAUTH_INFO_RESPONSE);
ssh2_userauth_ki_write_responses(
s, BinarySink_UPCAST(s->pktout));
s->pktout->minlen = 256;
pq_push(s->ppl.out_pq, s->pktout);
while (pktin->type == SSH2_MSG_USERAUTH_INFO_REQUEST) {
strbuf *amsg = authplugin_newmsg(
PLUGIN_KI_SERVER_REQUEST);
put_datapl(amsg, get_data(pktin, get_avail(pktin)));
authplugin_send_free(s, amsg);
/*
* Get the next packet in case it's another
* INFO_REQUEST.
*/
crMaybeWaitUntilV((pktin = ssh2_userauth_pop(s)) != NULL);
BinarySource src[1];
unsigned type;
while (true) {
crMaybeWaitUntilV(authplugin_expect_msg(
s, &type, src));
if (type != PLUGIN_KI_USER_REQUEST)
break;
if (!ssh2_userauth_ki_setup_prompts(s, src, true))
return;
crMaybeWaitUntilV(ssh2_userauth_ki_run_prompts(s));
if (spr_is_abort(s->spr)) {
/*
* Failed to get responses. Terminate.
*/
free_prompts(s->cur_prompt);
s->cur_prompt = NULL;
ssh_bpp_queue_disconnect(
s->ppl.bpp, "Unable to authenticate",
SSH2_DISCONNECT_AUTH_CANCELLED_BY_USER);
ssh_spr_close(
s->ppl.ssh, s->spr, "keyboard-"
"interactive authentication prompt");
return;
}
/*
* Send the responses on to the plugin.
*/
strbuf *amsg = authplugin_newmsg(
PLUGIN_KI_USER_RESPONSE);
ssh2_userauth_ki_write_responses(
s, BinarySink_UPCAST(amsg));
authplugin_send_free(s, amsg);
}
if (type != PLUGIN_KI_SERVER_RESPONSE) {
authplugin_bad_packet(
s, type, "expected PLUGIN_KI_SERVER_RESPONSE "
"or PLUGIN_PROTOCOL_USER_REQUEST");
return;
}
s->pktout = ssh_bpp_new_pktout(
s->ppl.bpp, SSH2_MSG_USERAUTH_INFO_RESPONSE);
put_datapl(s->pktout, get_data(src, get_avail(src)));
s->pktout->minlen = 256;
pq_push(s->ppl.out_pq, s->pktout);
/*
* Get the next packet in case it's another
* INFO_REQUEST.
*/
crMaybeWaitUntilV(
(pktin = ssh2_userauth_pop(s)) != NULL);
}
}
/*
@ -1411,7 +1752,9 @@ static void ssh2_userauth_process_queue(PacketProtocolLayer *ppl)
seat_set_trust_status(s->ppl.seat, true);
seat_antispoof_msg(
ppl_get_iseat(&s->ppl),
"End of keyboard-interactive prompts from server");
(s->authplugin_ki_active ?
"End of keyboard-interactive prompts from plugin" :
"End of keyboard-interactive prompts from server"));
}
/*
@ -1419,6 +1762,35 @@ static void ssh2_userauth_process_queue(PacketProtocolLayer *ppl)
*/
pq_push_front(s->ppl.in_pq, pktin);
if (s->authplugin_ki_active) {
/*
* As our last communication with the plugin, tell
* it whether the k-i authentication succeeded.
*/
int plugin_msg = -1;
if (pktin->type == SSH2_MSG_USERAUTH_SUCCESS) {
plugin_msg = PLUGIN_AUTH_SUCCESS;
} else if (pktin->type == SSH2_MSG_USERAUTH_FAILURE) {
/*
* Peek in the failure packet to see if it's a
* partial success.
*/
BinarySource src[1];
BinarySource_BARE_INIT(
src, get_ptr(pktin), get_avail(pktin));
get_string(pktin); /* skip methods */
bool partial_success = get_bool(pktin);
if (!get_err(src)) {
plugin_msg = partial_success ?
PLUGIN_AUTH_SUCCESS : PLUGIN_AUTH_FAILURE;
}
}
if (plugin_msg >= 0) {
strbuf *amsg = authplugin_newmsg(plugin_msg);
authplugin_send_free(s, amsg);
}
}
} else if (s->can_passwd) {
s->is_trivial_auth = false;
/*
@ -1695,7 +2067,7 @@ static void ssh2_userauth_process_queue(PacketProtocolLayer *ppl)
}
static bool ssh2_userauth_ki_setup_prompts(
struct ssh2_userauth_state *s, BinarySource *src)
struct ssh2_userauth_state *s, BinarySource *src, bool plugin)
{
ptrlen name, inst;
strbuf *sb;
@ -1721,14 +2093,17 @@ static bool ssh2_userauth_ki_setup_prompts(
bool echo = get_bool(src);
if (get_err(src)) {
ssh_proto_error(s->ppl.ssh, "Server sent truncated "
"SSH_MSG_USERAUTH_INFO_REQUEST packet");
ssh_proto_error(s->ppl.ssh, "%s sent truncated %s packet",
plugin ? "Plugin" : "Server",
plugin ? "PLUGIN_KI_USER_REQUEST" :
"SSH_MSG_USERAUTH_INFO_REQUEST");
return false;
}
sb = strbuf_new();
if (!prompt.len) {
put_datapl(sb, PTRLEN_LITERAL("<server failed to send prompt>: "));
put_fmt(sb, "<%s failed to send prompt>: ",
plugin ? "plugin" : "server");
} else if (s->ki_scc) {
stripctrl_retarget(s->ki_scc, BinarySink_UPCAST(sb));
put_datapl(s->ki_scc, prompt);
@ -1756,8 +2131,11 @@ static bool ssh2_userauth_ki_setup_prompts(
*/
if (!s->ki_printed_header && s->ki_scc &&
(s->num_prompts || name.len || inst.len)) {
seat_antispoof_msg(ppl_get_iseat(&s->ppl), "Keyboard-interactive "
"authentication prompts from server:");
seat_antispoof_msg(
ppl_get_iseat(&s->ppl),
(plugin ?
"Keyboard-interactive authentication prompts from plugin:" :
"Keyboard-interactive authentication prompts from server:"));
s->ki_printed_header = true;
seat_set_trust_status(s->ppl.seat, false);
}
@ -1773,7 +2151,11 @@ static bool ssh2_userauth_ki_setup_prompts(
}
s->cur_prompt->name_reqd = true;
} else {
put_datapl(sb, PTRLEN_LITERAL("SSH server authentication"));
if (plugin)
put_datapl(sb, PTRLEN_LITERAL(
"Communication with authentication plugin"));
else
put_datapl(sb, PTRLEN_LITERAL("SSH server authentication"));
s->cur_prompt->name_reqd = false;
}
s->cur_prompt->name = strbuf_to_str(sb);

View File

@ -120,6 +120,7 @@ typedef const char *HelpCtx;
#define WINHELP_CTX_ssh_no_trivial_userauth "config-ssh-notrivialauth"
#define WINHELP_CTX_ssh_auth_banner "config-ssh-banner"
#define WINHELP_CTX_ssh_auth_privkey "config-ssh-privkey"
#define WINHELP_CTX_ssh_auth_plugin "config-ssh-authplugin"
#define WINHELP_CTX_ssh_auth_cert "config-ssh-cert"
#define WINHELP_CTX_ssh_auth_agentfwd "config-ssh-agentfwd"
#define WINHELP_CTX_ssh_auth_changeuser "config-ssh-changeuser"