diff --git a/config.c b/config.c index c8e72d13..3885c7be 100644 --- a/config.c +++ b/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 diff --git a/contrib/authplugin-example.py b/contrib/authplugin-example.py new file mode 100755 index 00000000..395bd2c8 --- /dev/null +++ b/contrib/authplugin-example.py @@ -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 diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index 709df3de..2852d9c5 100644 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -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}) diff --git a/doc/authplugin.but b/doc/authplugin.but new file mode 100644 index 00000000..5ea4255f --- /dev/null +++ b/doc/authplugin.but @@ -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 : }, 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] diff --git a/doc/config.but b/doc/config.but index c2f8d0cb..786b4c5c 100644 --- a/doc/config.but +++ b/doc/config.but @@ -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 diff --git a/putty.h b/putty.h index 8f506fb6..5c3adfe9 100644 --- a/putty.h +++ b/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 \ diff --git a/settings.c b/settings.c index c1119702..44ec1978 100644 --- a/settings.c +++ b/settings.c @@ -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); diff --git a/ssh.h b/ssh.h index 79f1f431..e447665b 100644 --- a/ssh.h +++ b/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 */ +}; diff --git a/ssh/ppl.h b/ssh/ppl.h index 4ffeffe9..78b08efa 100644 --- a/ssh/ppl.h +++ b/ssh/ppl.h @@ -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, diff --git a/ssh/ssh.c b/ssh/ssh.c index aba09769..dbd6dde6 100644 --- a/ssh/ssh.c +++ b/ssh/ssh.c @@ -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; diff --git a/ssh/userauth2-client.c b/ssh/userauth2-client.c index 12910dbb..6bcc651a 100644 --- a/ssh/userauth2-client.c +++ b/ssh/userauth2-client.c @@ -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(": ")); + 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); diff --git a/windows/help.h b/windows/help.h index 799e6240..de6ec0be 100644 --- a/windows/help.h +++ b/windows/help.h @@ -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"