mirror of
https://git.tartarus.org/simon/putty.git
synced 2025-01-09 17:38: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:
parent
1f32a16dc8
commit
15f097f399
15
config.c
15
config.c
@ -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
287
contrib/authplugin-example.py
Executable 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
|
@ -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
519
doc/authplugin.but
Normal 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]
|
@ -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
|
||||
|
1
putty.h
1
putty.h
@ -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 \
|
||||
|
@ -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
32
ssh.h
@ -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 */
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user