1
0
mirror of https://git.tartarus.org/simon/putty.git synced 2025-07-01 11:32:48 -05:00

New feature: k-i authentication helper plugins.

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

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

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

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

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

View File

@ -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);