Add support for SASL mechanisms handled by scripts

This implements the suggested API from #323 with a caveat: currently the
SASL mechanisms are tied to IRC and needs a bit of extra work to make
them generic enough to move to the regular src/core instead of solely in
src/irc/core.

This greatly simplifies scripts for each mechanism since this handles
the splitting and reassembly of the 400 byte fragments going to and
coming from the server.

The ECDSA-NIST256P-CHALLENGE mechanism has been reimplemented using this
new API and found to work.
This commit is contained in:
Kenny Root 2016-06-28 18:26:02 -07:00
commit 7c37b2a4d6
14 changed files with 286 additions and 96 deletions

View file

@ -34,8 +34,8 @@
-cmdmax: Specifies the maximum number of commands to perform before
starting the internal flood protection.
-sasl_mechanism Specifies the mechanism to use for the SASL authentication.
At the moment irssi only supports the 'plain' and the
'external' mechanisms.
Irssi supports the 'plain' and 'external' mechanisms by
default and other mechanisms through scripts.
-sasl_username Specifies the username to use during the SASL authentication.
-sasl_password Specifies the password to use during the SASL authentication.

View file

@ -137,6 +137,9 @@ irc-cap.c
"server cap end", SERVER_REC
sasl.c
"server sasl init "<mechanism>, SERVER_REC
"server sasl step "<mechanism>, SERVER_REC, GBytes *
"server sasl abort "<mechanism>, SERVER_REC
"server sasl failure", SERVER_REC, char *reason
"server sasl success", SERVER_REC

View file

@ -8,7 +8,7 @@ char *address;
int port;
char *password;
int sasl_mechanism;
char *sasl_mechanism;
char *sasl_password;
char *ssl_cert;

View file

@ -88,26 +88,23 @@ static void sig_server_setup_fill_chatnet(IRC_SERVER_CONNECT_REC *conn,
conn->max_query_chans = ircnet->max_query_chans;
/* Validate the SASL parameters filled by sig_chatnet_read() or cmd_network_add */
conn->sasl_mechanism = SASL_MECHANISM_NONE;
g_free_and_null(conn->sasl_mechanism);
if (ircnet->sasl_mechanism != NULL) {
if (!g_ascii_strcasecmp(ircnet->sasl_mechanism, "plain")) {
/* The PLAIN method needs both the username and the password */
if (ircnet->sasl_username != NULL && *ircnet->sasl_username &&
ircnet->sasl_password != NULL && *ircnet->sasl_password) {
conn->sasl_mechanism = SASL_MECHANISM_PLAIN;
conn->sasl_username = ircnet->sasl_username;
conn->sasl_password = ircnet->sasl_password;
} else
g_warning("The fields sasl_username and sasl_password are either missing or empty");
conn->sasl_mechanism = g_strdup(ircnet->sasl_mechanism);
if (ircnet->sasl_username != NULL && *ircnet->sasl_username) {
g_free_and_null(conn->sasl_username);
conn->sasl_username = g_strdup(ircnet->sasl_username);
}
else if (!g_ascii_strcasecmp(ircnet->sasl_mechanism, "external")) {
conn->sasl_mechanism = SASL_MECHANISM_EXTERNAL;
conn->sasl_username = NULL;
conn->sasl_password = NULL;
if (ircnet->sasl_password != NULL && *ircnet->sasl_password) {
g_free_and_null(conn->sasl_password);
conn->sasl_password = g_strdup(ircnet->sasl_password);
}
else
g_warning("Unsupported SASL mechanism \"%s\" selected", ircnet->sasl_mechanism);
/* The PLAIN method needs both the username and the password */
if (!g_ascii_strcasecmp(ircnet->sasl_mechanism, "plain") &&
(conn->sasl_username == NULL || conn->sasl_password == NULL))
g_warning("The fields sasl_username and sasl_password are either missing or empty");
}
}

View file

@ -230,7 +230,7 @@ static void server_init(IRC_SERVER_REC *server)
g_free(cmd);
}
if (conn->sasl_mechanism != SASL_MECHANISM_NONE)
if (conn->sasl_mechanism != NULL)
cap_toggle(server, "sasl", TRUE);
cap_toggle(server, "multi-prefix", TRUE);

View file

@ -27,7 +27,7 @@ struct _IRC_SERVER_CONNECT_REC {
char *usermode;
char *alternate_nick;
int sasl_mechanism;
char *sasl_mechanism;
char *sasl_username;
char *sasl_password;

View file

@ -67,7 +67,7 @@ static void sig_session_save_server(IRC_SERVER_REC *server, CONFIG_REC *config,
config_node_set_str(config, node, "away_reason", server->away_reason);
config_node_set_bool(config, node, "emode_known", server->emode_known);
config_node_set_int(config, node, "sasl_mechanism", server->connrec->sasl_mechanism);
config_node_set_str(config, node, "sasl_mechanism", server->connrec->sasl_mechanism);
config_node_set_str(config, node, "sasl_username", server->connrec->sasl_username);
config_node_set_str(config, node, "sasl_password", server->connrec->sasl_password);
@ -96,10 +96,11 @@ static void sig_session_restore_server(IRC_SERVER_REC *server,
server->emode_known = config_node_get_bool(node, "emode_known", FALSE);
server->isupport_sent = config_node_get_bool(node, "isupport_sent", FALSE);
server->connrec->sasl_mechanism = config_node_get_int(node, "sasl_mechanism", SASL_MECHANISM_NONE);
/* The fields below might have been filled when loading the chatnet
* description from the config and we favor the content that's been saved
* in the session file over that. */
g_free(server->connrec->sasl_mechanism);
server->connrec->sasl_mechanism = g_strdup(config_node_get_str(node, "sasl_mechanism", NULL));
g_free(server->connrec->sasl_username);
server->connrec->sasl_username = g_strdup(config_node_get_str(node, "sasl_username", NULL));
g_free(server->connrec->sasl_password);

View file

@ -28,50 +28,131 @@
#define SASL_TIMEOUT (20 * 1000) // ms
static gboolean sasl_timeout(IRC_SERVER_REC *server)
static GSList *sasl_mechanisms;
static GHashTable *sasl_buffers;
void sasl_mechanism_register(const char *name)
{
if (gslist_find_string(sasl_mechanisms, name) == NULL) {
sasl_mechanisms = g_slist_append(sasl_mechanisms, g_strdup(name));
}
}
void sasl_mechanism_unregister(const char *name)
{
GSList *pos;
pos = gslist_find_string(sasl_mechanisms, name);
if (pos != NULL) {
g_free(pos->data);
sasl_mechanisms = g_slist_remove(sasl_mechanisms, pos->data);
}
}
static gboolean is_registered_mechanism(const char *name)
{
return gslist_find_string(sasl_mechanisms, name) != NULL;
}
static void sasl_emit_abort_signal(IRC_SERVER_REC *server)
{
IRC_SERVER_CONNECT_REC *conn;
conn = server->connrec;
if (is_registered_mechanism(conn->sasl_mechanism)) {
char *str;
str = g_strconcat("server sasl abort ", conn->sasl_mechanism, NULL);
ascii_strdown(str+18);
if (!signal_emit(str, 1, server)) {
g_warning("No one handled the abort of SASL mechanism %s",
conn->sasl_mechanism);
}
g_free(str);
}
}
/*
* This is the callback from g_timeout_add.
*/
static gboolean sasl_timeout_callback(IRC_SERVER_REC *server)
{
/* The authentication timed out, we can't do much beside terminating it */
irc_send_cmd_now(server, "AUTHENTICATE *");
cap_finish_negotiation(server);
server->sasl_timeout = 0;
sasl_abort(server);
signal_emit("server sasl failure", 2, server, "The authentication timed out");
return FALSE;
}
static void sasl_timeout_cancel(IRC_SERVER_REC *server)
{
if (server->sasl_timeout != 0) {
g_source_remove(server->sasl_timeout);
server->sasl_timeout = 0;
}
}
static void sasl_timeout_start(IRC_SERVER_REC *server)
{
sasl_timeout_cancel(server);
server->sasl_timeout = g_timeout_add_seconds(SASL_TIMEOUT, (GSourceFunc) sasl_timeout_callback,
server);
}
void sasl_abort(IRC_SERVER_REC *server)
{
sasl_timeout_cancel(server);
irc_send_cmd_now(server, "AUTHENTICATE *");
cap_finish_negotiation(server);
sasl_emit_abort_signal(server);
}
static void sasl_start(IRC_SERVER_REC *server, const char *data, const char *from)
{
IRC_SERVER_CONNECT_REC *conn;
conn = server->connrec;
switch (conn->sasl_mechanism) {
case SASL_MECHANISM_PLAIN:
irc_send_cmd_now(server, "AUTHENTICATE PLAIN");
break;
if (!g_strcmp0(conn->sasl_mechanism, "PLAIN")) {
irc_send_cmd_now(server, "AUTHENTICATE PLAIN");
sasl_timeout_start(server);
} else if (!g_strcmp0(conn->sasl_mechanism, "external")) {
irc_send_cmd_now(server, "AUTHENTICATE EXTERNAL");
sasl_timeout_start(server);
} else if (is_registered_mechanism(conn->sasl_mechanism)) {
char *str;
case SASL_MECHANISM_EXTERNAL:
irc_send_cmd_now(server, "AUTHENTICATE EXTERNAL");
break;
str = g_strconcat("server sasl init ", conn->sasl_mechanism, NULL);
ascii_strdown(str+17);
if (!signal_emit(str, 1, server)) {
g_warning("Nothing handled initialization of SASL mechanism %s",
conn->sasl_mechanism);
}
g_free(str);
str = g_strconcat("AUTHENTICATE ", conn->sasl_mechanism, NULL);
irc_send_cmd_now(server, str);
g_free(str);
sasl_timeout_start(server);
} else {
g_warning("Unsupported SASL mechanism \"%s\" selected", conn->sasl_mechanism);
sasl_abort(server);
}
server->sasl_timeout = g_timeout_add(SASL_TIMEOUT, (GSourceFunc) sasl_timeout, server);
}
static void sasl_fail(IRC_SERVER_REC *server, const char *data, const char *from)
{
char *params, *error;
/* Stop any pending timeout, if any */
if (server->sasl_timeout != 0) {
g_source_remove(server->sasl_timeout);
server->sasl_timeout = 0;
}
sasl_timeout_cancel(server);
params = event_get_params(data, 2, NULL, &error);
signal_emit("server sasl failure", 2, server, error);
sasl_emit_abort_signal(server);
/* Terminate the negotiation */
cap_finish_negotiation(server);
@ -81,12 +162,10 @@ static void sasl_fail(IRC_SERVER_REC *server, const char *data, const char *from
static void sasl_already(IRC_SERVER_REC *server, const char *data, const char *from)
{
if (server->sasl_timeout != 0) {
g_source_remove(server->sasl_timeout);
server->sasl_timeout = 0;
}
sasl_timeout_cancel(server);
signal_emit("server sasl success", 1, server);
sasl_emit_abort_signal(server);
/* We're already authenticated, do nothing */
cap_finish_negotiation(server);
@ -94,10 +173,7 @@ static void sasl_already(IRC_SERVER_REC *server, const char *data, const char *f
static void sasl_success(IRC_SERVER_REC *server, const char *data, const char *from)
{
if (server->sasl_timeout != 0) {
g_source_remove(server->sasl_timeout);
server->sasl_timeout = 0;
}
sasl_timeout_cancel(server);
signal_emit("server sasl success", 1, server);
@ -105,52 +181,112 @@ static void sasl_success(IRC_SERVER_REC *server, const char *data, const char *f
cap_finish_negotiation(server);
}
static void sasl_send_step_if_complete(IRC_SERVER_REC *server, const char *enc_req)
{
char *buffer;
size_t enc_req_len;
enc_req_len = strlen(enc_req);
buffer = g_hash_table_lookup(sasl_buffers, server->tag);
if (buffer != NULL) {
if (!g_strcmp0("+", enc_req)) {
enc_req = buffer;
} else {
enc_req = g_strconcat(buffer, enc_req, NULL);
}
g_hash_table_remove(sasl_buffers, server->tag);
}
if (enc_req_len == 400) {
g_hash_table_insert(sasl_buffers, server->tag, g_strdup(enc_req));
} else {
gchar *output;
GBytes *decoded;
IRC_SERVER_CONNECT_REC *conn;
if (!g_strcmp0("+", enc_req)) {
decoded = g_bytes_new("", 0);
} else {
gsize dec_len;
guchar *tmp;
tmp = g_base64_decode(enc_req, &dec_len);
decoded = g_bytes_new(tmp, dec_len);
}
conn = server->connrec;
output = g_strconcat("server sasl step ", conn->sasl_mechanism, NULL);
ascii_strdown(output+17);
if (!signal_emit(output, 2, server, decoded)) {
g_warning("No one can handle SASL mechanism: %s",
conn->sasl_mechanism);
}
g_free(output);
g_bytes_unref(decoded);
}
g_free_not_null(buffer);
}
void sasl_send_response(IRC_SERVER_REC *server, GBytes *response)
{
char *enc;
const guchar *p;
gsize len;
size_t offset, enc_len;
p = g_bytes_get_data(response, &len);
enc = g_base64_encode(p, len);
enc_len = strlen(enc);
offset = 0;
while (offset < enc_len) {
irc_send_cmdv(server, "AUTHENTICATE %400s",
offset == enc_len ? "+" : (enc + offset));
offset += 400;
}
g_free(enc);
}
static void sasl_step(IRC_SERVER_REC *server, const char *data, const char *from)
{
IRC_SERVER_CONNECT_REC *conn;
GString *req;
char *enc_req;
conn = server->connrec;
/* Stop the timer */
if (server->sasl_timeout != 0) {
g_source_remove(server->sasl_timeout);
server->sasl_timeout = 0;
}
sasl_timeout_cancel(server);
switch (conn->sasl_mechanism) {
case SASL_MECHANISM_PLAIN:
/* At this point we assume that conn->sasl_{username, password} are non-NULL.
* The PLAIN mechanism expects a NULL-separated string composed by the authorization identity, the
* authentication identity and the password.
* The authorization identity field is explicitly set to the user provided username.
* The whole request is then encoded in base64. */
if (!g_strcmp0(conn->sasl_mechanism, "plain")) {
/* At this point we assume that conn->sasl_{username, password} are non-NULL.
* The PLAIN mechanism expects a NULL-separated string composed by the authorization identity, the
* authentication identity and the password.
* The authorization identity field is explicitly set to the user provided username.
* The whole request is then encoded in base64. */
GBytes *bytes;
req = g_string_new(NULL);
req = g_string_new(NULL);
g_string_append(req, conn->sasl_username);
g_string_append_c(req, '\0');
g_string_append(req, conn->sasl_username);
g_string_append_c(req, '\0');
g_string_append(req, conn->sasl_password);
g_string_append(req, conn->sasl_username);
g_string_append_c(req, '\0');
g_string_append(req, conn->sasl_username);
g_string_append_c(req, '\0');
g_string_append(req, conn->sasl_password);
enc_req = g_base64_encode((const guchar *)req->str, req->len);
irc_send_cmdv(server, "AUTHENTICATE %s", enc_req);
g_free(enc_req);
g_string_free(req, TRUE);
break;
case SASL_MECHANISM_EXTERNAL:
/* Empty response */
irc_send_cmdv(server, "AUTHENTICATE +");
break;
bytes = g_bytes_new(req->str, req->len);
sasl_send_response(server, bytes);
g_string_free(req, TRUE);
} else if (!g_strcmp0(conn->sasl_mechanism, "external")) {
/* Empty response */
irc_send_cmdv(server, "AUTHENTICATE +");
} else {
sasl_send_step_if_complete(server, data);
}
/* We expect a response within a reasonable time */
server->sasl_timeout = g_timeout_add(SASL_TIMEOUT, (GSourceFunc) sasl_timeout, server);
sasl_timeout_start(server);
}
static void sasl_disconnected(IRC_SERVER_REC *server)
@ -161,14 +297,15 @@ static void sasl_disconnected(IRC_SERVER_REC *server)
return;
}
if (server->sasl_timeout != 0) {
g_source_remove(server->sasl_timeout);
server->sasl_timeout = 0;
}
sasl_timeout_cancel(server);
sasl_emit_abort_signal(server);
}
void sasl_init(void)
{
sasl_buffers = g_hash_table_new((GHashFunc) g_str_hash, (GCompareFunc) g_str_equal);
signal_add_first("server cap ack sasl", (SIGNAL_FUNC) sasl_start);
signal_add_first("event authenticate", (SIGNAL_FUNC) sasl_step);
signal_add_first("event 903", (SIGNAL_FUNC) sasl_success);
@ -182,6 +319,8 @@ void sasl_init(void)
void sasl_deinit(void)
{
g_hash_table_destroy(sasl_buffers);
signal_remove("server cap ack sasl", (SIGNAL_FUNC) sasl_start);
signal_remove("event authenticate", (SIGNAL_FUNC) sasl_step);
signal_remove("event 903", (SIGNAL_FUNC) sasl_success);

View file

@ -21,12 +21,10 @@
#ifndef __SASL_H
#define __SASL_H
enum {
SASL_MECHANISM_NONE = 0,
SASL_MECHANISM_PLAIN,
SASL_MECHANISM_EXTERNAL,
SASL_MECHANISM_MAX
};
void sasl_mechanism_register(const char *);
void sasl_mechanism_unregister(const char *);
void sasl_send_response(IRC_SERVER_REC *, GBytes *);
void sasl_abort(IRC_SERVER_REC *);
void sasl_init(void);
void sasl_deinit(void);

View file

@ -18,6 +18,7 @@ while (<STDIN>) {
s/GList \* of ([^,]*)/glistptr_\1/g;
s/GSList of (\w+)s/gslist_\1/g;
s/GBytes \*/gbytes/g;
s/char \*[^,]*/string/g;
s/ulong \*[^,]*/ulongptr/g;

View file

@ -7,6 +7,9 @@ static void perl_irc_connect_fill_hash(HV *hv, IRC_SERVER_CONNECT_REC *conn)
{
perl_connect_fill_hash(hv, (SERVER_CONNECT_REC *) conn);
(void) hv_store(hv, "alternate_nick", 14, new_pv(conn->alternate_nick), 0);
(void) hv_store(hv, "sasl_username", 13, new_pv(conn->sasl_username), 0);
(void) hv_store(hv, "sasl_password", 13, new_pv(conn->sasl_password), 0);
}
static void perl_irc_server_fill_hash(HV *hv, IRC_SERVER_REC *server)
@ -227,3 +230,11 @@ BOOT:
irssi_boot(Irc__Query);
irssi_boot(Irc__Server);
irssi_boot(Irc__Client);
void
sasl_mechanism_register(name)
char *name
void
sasl_mechanism_unregister(name)
char *name

View file

@ -149,3 +149,25 @@ CODE:
OUTPUT:
RETVAL
MODULE = Irssi::Irc::Server PACKAGE = Irssi::Irc::Server
PROTOTYPES: ENABLE
void
send_sasl_response(server, response)
Irssi::Irc::Server server
SV *response
PREINIT:
char *p;
STRLEN len;
GBytes *bytes;
CODE:
p = SvPV(response, len);
bytes = g_bytes_new(p, len);
sasl_send_response(server, bytes);
g_bytes_unref(bytes);
void
send_sasl_abort(server)
Irssi::Irc::Server server
CODE:
sasl_abort(server);

View file

@ -12,6 +12,7 @@
#include "mode-lists.h"
#include "netsplit.h"
#include "servers-redirect.h"
#include "sasl.h"
#include "dcc/dcc.h"
#include "dcc/dcc-file.h"

View file

@ -80,6 +80,7 @@ void perl_signal_args_to_c(
unsigned long v_ulong;
GSList *v_gslist;
GList *v_glist;
GBytes *v_gbytes;
} saved_args[SIGNAL_MAX_ARGUMENTS];
void *p[SIGNAL_MAX_ARGUMENTS];
PERL_SIGNAL_ARGS_REC *rec;
@ -101,6 +102,12 @@ void perl_signal_args_to_c(
c_arg = NULL;
} else if (g_strcmp0(rec->args[n], "string") == 0) {
c_arg = SvPV_nolen(arg);
} else if (g_strcmp0(rec->args[n], "gbytes") == 0) {
char *bytes;
STRLEN len;
bytes = SvPV(arg, len);
c_arg = saved_args[n].v_gbytes = g_bytes_new(bytes, len);
} else if (g_strcmp0(rec->args[n], "int") == 0) {
c_arg = (void *)SvIV(arg);
} else if (g_strcmp0(rec->args[n], "ulongptr") == 0) {
@ -181,7 +188,9 @@ void perl_signal_args_to_c(
continue;
}
if (g_strcmp0(rec->args[n], "intptr") == 0) {
if (g_strcmp0(rec->args[n], "gbytes") == 0) {
g_bytes_unref(saved_args[n].v_gbytes);
} else if (g_strcmp0(rec->args[n], "intptr") == 0) {
SV *t = SvRV(arg);
SvIOK_only(t);
SvIV_set(t, saved_args[n].v_int);
@ -291,6 +300,14 @@ static void perl_call_signal(PERL_SCRIPT_REC *script, SV *func,
/* "simple irssi object" - any struct that has
int type; as it's first variable (dcc) */
perlarg = simple_iobject_bless((SERVER_REC *) arg);
} else if (g_strcmp0(rec->args[n], "gbytes") == 0) {
GBytes *tmp;
const char *data;
STRLEN data_len;
tmp = arg;
data = g_bytes_get_data(tmp, &data_len);
perlarg = newSVpvn(data, data_len);
} else {
/* blessed object */
perlarg = plain_bless(arg, rec->args[n]);