pissircd/src/modules/extjwt.c

1152 lines
31 KiB
C

/*
* IRC - Internet Relay Chat, src/modules/extjwt.c
* (C) 2021 The UnrealIRCd Team
*
* See file AUTHORS in IRC package for additional names of
* the programmers.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 1, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#include "unrealircd.h"
#if defined(__GNUC__)
/* Temporarily ignore these for this entire file. FIXME later when updating the code for OpenSSL 3: */
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif
/* internal definitions */
#define MSG_EXTJWT "EXTJWT"
#define MYCONF "extjwt"
#undef NEW_ISUPPORT /* enable this for https://github.com/ircv3/ircv3-specifications/pull/341#issuecomment-617038799 */
#define EXTJWT_METHOD_NOT_SET 0
#define EXTJWT_METHOD_HS256 1
#define EXTJWT_METHOD_HS384 2
#define EXTJWT_METHOD_HS512 3
#define EXTJWT_METHOD_RS256 4
#define EXTJWT_METHOD_RS384 5
#define EXTJWT_METHOD_RS512 6
#define EXTJWT_METHOD_ES256 7
#define EXTJWT_METHOD_ES384 8
#define EXTJWT_METHOD_ES512 9
#define EXTJWT_METHOD_NONE 10
#define NEEDS_KEY(x) (x>=EXTJWT_METHOD_RS256 && x<=EXTJWT_METHOD_ES512)
#define URL_LENGTH 4096
#define MODES_SIZE 41 /* about 10 mode chars */
#define TS_LENGTH 19 /* 64-bit integer */
#define MAX_TOKEN_CHUNK (510-sizeof(extjwt_message_pattern)-HOSTLEN-CHANNELLEN)
/* OpenSSL 1.0.x compatibility */
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
void ECDSA_SIG_get0(const ECDSA_SIG *sig, const BIGNUM **pr, const BIGNUM **ps)
{
if (pr != NULL)
*pr = sig->r;
if (ps != NULL)
*ps = sig->s;
}
#endif
/* struct definitions */
struct extjwt_config {
time_t exp_delay;
char *secret;
int method;
char *vfy;
};
struct jwt_service {
char *name;
struct extjwt_config *cfg;
struct jwt_service *next;
};
/* function declarations */
CMD_FUNC(cmd_extjwt);
char *extjwt_make_payload(Client *client, Channel *channel, struct extjwt_config *config);
char *extjwt_generate_token(const char *payload, struct extjwt_config *config);
void b64url(char *b64);
unsigned char *extjwt_hmac_extjwt_hash(int method, const void *key, int keylen, const unsigned char *data, int datalen, unsigned int* resultlen);
unsigned char *extjwt_sha_pem_extjwt_hash(int method, const void *key, int keylen, const unsigned char *data, int datalen, unsigned int* resultlen);
unsigned char *extjwt_hash(int method, const void *key, int keylen, const unsigned char *data, int datalen, unsigned int* resultlen);
char *extjwt_gen_header(int method);
int extjwt_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
int extjwt_configrun(ConfigFile *cf, ConfigEntry *ce, int type);
int extjwt_configposttest(int *errs);
void extjwt_free_services(struct jwt_service **services);
struct jwt_service *find_jwt_service(struct jwt_service *services, const char *name);
int extjwt_valid_integer_string(const char *in, int min, int max);
char *extjwt_test_key(const char *file, int method);
char *extjwt_read_file_contents(const char *file, int absolute, int *size);
int EXTJWT_METHOD_from_string(const char *in);
#ifdef NEW_ISUPPORT
char *extjwt_isupport_param(void);
#endif
/* string constants */
const char extjwt_message_pattern[] = ":%s EXTJWT %s %s %s%s";
/* global structs */
ModuleHeader MOD_HEADER = {
"extjwt",
"6.0",
"Command /EXTJWT (web service authorization)",
"UnrealIRCd Team",
"unrealircd-6"
};
struct {
int have_secret;
int have_key;
int have_method;
int have_expire;
int have_vfy;
char *key_filename;
} cfg_state;
struct extjwt_config cfg;
struct jwt_service *jwt_services;
MOD_TEST()
{
memset(&cfg_state, 0, sizeof(cfg_state));
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, extjwt_configtest);
HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, extjwt_configposttest);
return MOD_SUCCESS;
}
MOD_INIT()
{
CommandAdd(modinfo->handle, MSG_EXTJWT, cmd_extjwt, 2, CMD_USER);
HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, extjwt_configrun);
return MOD_SUCCESS;
}
MOD_LOAD()
{
struct jwt_service *service = jwt_services;
#ifdef NEW_ISUPPORT
ISupportAdd(modinfo->handle, "EXTJWT", extjwt_isupport_param());
#else
ISupportAdd(modinfo->handle, "EXTJWT", "1");
#endif
while (service)
{ /* copy default exp to all services not having one specified */
if (service->cfg->exp_delay == 0)
service->cfg->exp_delay = cfg.exp_delay;
service = service->next;
}
return MOD_SUCCESS;
}
MOD_UNLOAD()
{
extjwt_free_services(&jwt_services);
return MOD_SUCCESS;
}
#ifdef NEW_ISUPPORT
char *extjwt_isupport_param(void)
{
struct jwt_service *services = jwt_services;
int count = 0;
static char buf[500];
strlcpy(buf, "V:1", sizeof(buf));
while (services)
{
strlcat(buf, count?",":"&S:", sizeof(buf));
strlcat(buf, services->name, sizeof(buf));
count++;
services = services->next;
}
return buf;
}
#endif
void extjwt_free_services(struct jwt_service **services){
struct jwt_service *ss, *next;
ss = *services;
while (ss)
{
next = ss->next;
safe_free(ss->name);
if (ss->cfg)
safe_free(ss->cfg->secret);
safe_free(ss->cfg);
safe_free(ss);
ss = next;
}
*services = NULL;
}
struct jwt_service *find_jwt_service(struct jwt_service *services, const char *name)
{
if (!name)
return NULL;
while (services)
{
if (services->name && !strcmp(services->name, name))
return services;
services = services->next;
}
return NULL;
}
int extjwt_valid_integer_string(const char *in, int min, int max)
{
int i, val;
if (BadPtr(in))
return 0;
for (i=0; in[i]; i++){
if (!isdigit(in[i]))
return 0;
}
val = atoi(in);
if (val < min || val > max)
return 0;
return 1;
}
int vfy_url_is_valid(const char *string)
{
if (strstr(string, "http://") == string || strstr(string, "https://") == string)
{
if (strstr(string, "%s"))
return 1;
}
return 0;
}
char *extjwt_test_key(const char *file, int method)
{ /* returns NULL when valid */
int fsize;
char *fcontent = NULL;
char *retval = NULL;
BIO *bufkey = NULL;
EVP_PKEY *pkey = NULL;
int type, pkey_type;
do {
switch (method)
{
case EXTJWT_METHOD_RS256: case EXTJWT_METHOD_RS384: case EXTJWT_METHOD_RS512:
type = EVP_PKEY_RSA;
break;
case EXTJWT_METHOD_ES256: case EXTJWT_METHOD_ES384: case EXTJWT_METHOD_ES512:
type = EVP_PKEY_EC;
break;
default:
retval = "Internal error (invalid type)";
return retval;
}
fcontent = extjwt_read_file_contents(file, 0, &fsize);
if (!fcontent)
{
retval = "Cannot open file";
break;
}
if (fsize == 0)
{
retval = "File is empty";
break;
}
if (!(bufkey = BIO_new_mem_buf(fcontent, fsize)))
{
retval = "Unknown error";
break;
}
if (!(pkey = PEM_read_bio_PrivateKey(bufkey, NULL, NULL, NULL)))
{
retval = "Key is invalid";
break;
}
pkey_type = EVP_PKEY_id(pkey);
if (type != pkey_type)
{
retval = "Key does not match method";
break;
}
} while (0);
safe_free(fcontent);
if (bufkey)
BIO_free(bufkey);
if (pkey)
EVP_PKEY_free(pkey);
return retval;
}
int EXTJWT_METHOD_from_string(const char *in)
{
if (!strcmp(in, "HS256"))
return EXTJWT_METHOD_HS256;
if (!strcmp(in, "HS384"))
return EXTJWT_METHOD_HS384;
if (!strcmp(in, "HS512"))
return EXTJWT_METHOD_HS512;
if (!strcmp(in, "RS256"))
return EXTJWT_METHOD_RS256;
if (!strcmp(in, "RS384"))
return EXTJWT_METHOD_RS384;
if (!strcmp(in, "RS512"))
return EXTJWT_METHOD_RS512;
if (!strcmp(in, "ES256"))
return EXTJWT_METHOD_ES256;
if (!strcmp(in, "ES384"))
return EXTJWT_METHOD_ES384;
if (!strcmp(in, "ES512"))
return EXTJWT_METHOD_ES512;
if (!strcmp(in, "NONE"))
return EXTJWT_METHOD_NONE;
return EXTJWT_METHOD_NOT_SET;
}
/* Configuration is described in conf/modules.optional.conf */
int extjwt_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
{
int errors = 0;
ConfigEntry *cep, *cep2;
int i;
struct jwt_service *services = NULL;
struct jwt_service **ss = &services; /* list for checking whether service names repeat */
int have_ssecret, have_smethod, have_svfy, have_scert;
unsigned int sfilename_line_number = 0;
char *sfilename = NULL;
if (type != CONFIG_MAIN)
return 0;
if (!ce || strcmp(ce->name, MYCONF))
return 0;
for (cep = ce->items; cep; cep = cep->next)
{
if (!cep->value)
{
config_error("%s:%i: blank %s::%s without value", cep->file->filename, cep->line_number, MYCONF, cep->name);
errors++;
continue;
}
if (!strcmp(cep->name, "method"))
{
if (cfg_state.have_method)
{
config_error("%s:%i: duplicate %s::%s item", cep->file->filename, cep->line_number, MYCONF, cep->name);
errors++;
continue;
}
cfg_state.have_method = EXTJWT_METHOD_from_string(cep->value);
if (cfg_state.have_method == EXTJWT_METHOD_NOT_SET)
{
config_error("%s:%i: invalid value %s::%s \"%s\" (check docs for allowed options)", cep->file->filename, cep->line_number, MYCONF, cep->name, cep->value);
errors++;
}
continue;
}
if (!strcmp(cep->name, "expire-after"))
{
if (!extjwt_valid_integer_string(cep->value, 1, 9999))
{
config_error("%s:%i: %s::%s must be an integer between 1 and 9999 (seconds)", cep->file->filename, cep->line_number, MYCONF, cep->name);
errors++;
}
continue;
}
if (!strcmp(cep->name, "secret"))
{
if (cfg_state.have_secret)
{
config_error("%s:%i: duplicate %s::%s item", cep->file->filename, cep->line_number, MYCONF, cep->name);
errors++;
continue;
}
cfg_state.have_secret = 1;
if (strlen(cep->value) < 4)
{
config_error("%s:%i: Secret specified in %s::%s is too short!", cep->file->filename, cep->line_number, MYCONF, cep->name);
errors++;
}
continue;
}
if (!strcmp(cep->name, "key"))
{
if (cfg_state.have_key)
{
config_error("%s:%i: duplicate %s::%s item", cep->file->filename, cep->line_number, MYCONF, cep->name);
errors++;
continue;
}
if (!is_file_readable(cep->value, CONFDIR))
{
config_error("%s:%i: Cannot open file \"%s\" specified in %s::%s for reading", cep->file->filename, cep->line_number, cep->value, MYCONF, cep->name);
errors++;
}
safe_strdup(cfg_state.key_filename, cep->value);
cfg_state.have_key = 1;
continue;
}
if (!strcmp(cep->name, "verify-url"))
{
if (cfg_state.have_vfy)
{
config_error("%s:%i: duplicate %s:%s item", cep->file->filename, cep->line_number, MYCONF, cep->name);
errors++;
continue;
}
cfg_state.have_vfy = 1;
if (!vfy_url_is_valid(cep->value))
{
config_error("%s:%i: Optional URL specified in %s::%s is invalid!", cep->file->filename, cep->line_number, MYCONF, cep->name);
errors++;
continue;
}
if (strlen(cep->value) > URL_LENGTH)
{
config_error("%s:%i: Optional URL specified in %s::%s is too long!", cep->file->filename, cep->line_number, MYCONF, cep->name);
errors++;
}
continue;
}
if (!strcmp(cep->name, "service"))
{
have_ssecret = 0;
have_smethod = 0;
have_svfy = 0;
have_scert = 0;
if (strchr(cep->value, ' ') || strchr(cep->value, ','))
{
config_error("%s:%i: Invalid %s::%s name (contains spaces or commas)", cep->file->filename, cep->line_number, MYCONF, cep->name);
errors++;
continue;
}
if (find_jwt_service(services, cep->value))
{
config_error("%s:%i: Duplicate %s::%s name \"%s\"", cep->file->filename, cep->line_number, MYCONF, cep->name, cep->value);
errors++;
continue;
}
*ss = safe_alloc(sizeof(struct jwt_service)); /* store the new name for further checking */
safe_strdup((*ss)->name, cep->value);
ss = &(*ss)->next;
for (cep2 = cep->items; cep2; cep2 = cep2->next)
{
if (!cep2->name || !cep2->value || !cep2->value[0])
{
config_error("%s:%i: blank/incomplete %s::service entry", cep2->file->filename, cep2->line_number, MYCONF);
errors++;
continue;
}
if (!strcmp(cep2->name, "method"))
{
if (have_smethod)
{
config_error("%s:%i: duplicate %s::service::%s item", cep2->file->filename, cep2->line_number, MYCONF, cep2->name);
errors++;
continue;
}
have_smethod = EXTJWT_METHOD_from_string(cep2->value);
if (have_smethod == EXTJWT_METHOD_NOT_SET || have_smethod == EXTJWT_METHOD_NONE)
{
config_error("%s:%i: invalid value of optional %s::service::%s \"%s\" (check docs for allowed options)", cep2->file->filename, cep2->line_number, MYCONF, cep2->name, cep2->value);
errors++;
}
continue;
}
if (!strcmp(cep2->name, "secret"))
{
if (have_ssecret)
{
config_error("%s:%i: duplicate %s::service::%s item", cep2->file->filename, cep2->line_number, MYCONF, cep2->name);
errors++;
continue;
}
have_ssecret = 1;
if (strlen(cep2->value) < 4) /* TODO maybe a better check? */
{
config_error("%s:%i: Secret specified in %s::service::%s is too short!", cep2->file->filename, cep2->line_number, MYCONF, cep2->name);
errors++;
}
continue;
}
if (!strcmp(cep2->name, "key"))
{
if (have_scert)
{
config_error("%s:%i: duplicate %s::service::%s item", cep2->file->filename, cep2->line_number, MYCONF, cep2->name);
errors++;
continue;
}
if (!is_file_readable(cep2->value, CONFDIR))
{
config_error("%s:%i: Cannot open file \"%s\" specified in %s::service::%s for reading", cep2->file->filename, cep2->line_number, cep2->value, MYCONF, cep2->name);
errors++;
}
have_scert = 1;
safe_strdup(sfilename, cep2->value);
sfilename_line_number = cep2->line_number;
continue;
}
if (!strcmp(cep2->name, "expire-after"))
{
if (!extjwt_valid_integer_string(cep2->value, 1, 9999))
{
config_error("%s:%i: %s::%s must be an integer between 1 and 9999 (seconds)", cep2->file->filename, cep2->line_number, MYCONF, cep2->name);
errors++;
}
continue;
}
if (!strcmp(cep2->name, "verify-url"))
{
if (have_svfy)
{
config_error("%s:%i: duplicate %s::service::%s item", cep2->file->filename, cep2->line_number, MYCONF, cep2->name);
errors++;
continue;
}
have_svfy = 1;
if (!vfy_url_is_valid(cep2->value))
{
config_error("%s:%i: Optional URL specified in %s::service::%s is invalid!", cep2->file->filename, cep2->line_number, MYCONF, cep2->name);
errors++;
continue;
}
if (strlen(cep2->value) > URL_LENGTH)
{
config_error("%s:%i: Optional URL specified in %s::service::%s is too long!", cep2->file->filename, cep2->line_number, MYCONF, cep2->name);
errors++;
}
continue;
}
config_error("%s:%i: invalid %s::service attribute %s (must be one of: name, secret, expire-after)", cep2->file->filename, cep2->line_number, MYCONF, cep2->name);
errors++;
}
if (!have_smethod)
{
config_error("%s:%i: invalid %s::service entry (no %s::service::method specfied)", cep->file->filename, cep->line_number, MYCONF, MYCONF);
errors++;
continue;
}
if (have_ssecret && NEEDS_KEY(have_smethod))
{
config_error("%s:%i: invalid %s::service entry (this method needs %s::service::key and not %s::service::secret option)", cep->file->filename, cep->line_number, MYCONF, MYCONF, MYCONF);
errors++;
continue;
}
if (have_scert && !NEEDS_KEY(have_smethod))
{
config_error("%s:%i: invalid %s::service entry (this method needs %s::service::secret and not %s::service::key option)", cep->file->filename, cep->line_number, MYCONF, MYCONF, MYCONF);
errors++;
continue;
}
if (!have_ssecret && !NEEDS_KEY(have_smethod))
{
config_error("%s:%i: invalid %s::service entry (must contain %s::service::secret option)", cep->file->filename, cep->line_number, MYCONF, MYCONF);
errors++;
continue;
}
if (!have_scert && NEEDS_KEY(have_smethod)) {
config_error("%s:%i: invalid %s::service entry (must contain %s::service::key option)", cep->file->filename, cep->line_number, MYCONF, MYCONF);
errors++;
continue;
}
if (NEEDS_KEY(have_smethod) && have_scert)
{
char *keyerr;
keyerr = extjwt_test_key(sfilename, have_smethod);
if (keyerr)
{
config_error("%s:%i: Invalid key file specified for %s::key: %s", cep->file->filename, sfilename_line_number, MYCONF, keyerr);
errors++;
}
}
continue;
}
config_error("%s:%i: unknown directive %s::%s", cep->file->filename, cep->line_number, MYCONF, cep->name);
errors++;
}
*errs = errors;
extjwt_free_services(&services);
if (errors)
safe_free(cfg_state.key_filename);
safe_free(sfilename);
return errors ? -1 : 1;
}
int extjwt_configposttest(int *errs)
{
int errors = 0;
if (cfg_state.have_method == EXTJWT_METHOD_NOT_SET)
{
config_error("No %s::method specfied!", MYCONF);
errors++;
} else
{
if (cfg_state.have_method != EXTJWT_METHOD_NONE && !NEEDS_KEY(cfg_state.have_method) && !cfg_state.have_secret)
{
config_error("No %s::secret specfied as required by requested method!", MYCONF);
errors++;
}
if ((cfg_state.have_method == EXTJWT_METHOD_NONE || NEEDS_KEY(cfg_state.have_method)) && cfg_state.have_secret)
{
config_error("A %s::secret specfied but it should not be when using requested method!", MYCONF);
errors++;
}
if (NEEDS_KEY(cfg_state.have_method) && !cfg_state.have_key)
{
config_error("No %s::key specfied as required by requested method!", MYCONF);
errors++;
}
if (!NEEDS_KEY(cfg_state.have_method) && cfg_state.have_key)
{
config_error("A %s::key specfied but it should not be when using requested method!", MYCONF);
errors++;
}
if (NEEDS_KEY(cfg_state.have_method) && cfg_state.have_key && cfg_state.key_filename)
{
char *keyerr;
keyerr = extjwt_test_key(cfg_state.key_filename, cfg_state.have_method);
if (keyerr)
{
config_error("Invalid key file specified for %s::key: %s", MYCONF, keyerr);
errors++;
}
}
}
safe_free(cfg_state.key_filename);
if (errors)
{
*errs = errors;
return -1;
}
/* setting defaults, FIXME this may behave incorrectly if there's another module failing POSTTEST */
if (!cfg_state.have_expire)
cfg.exp_delay = 30;
/* prepare service list to load new data */
extjwt_free_services(&jwt_services);
return 1;
}
int extjwt_configrun(ConfigFile *cf, ConfigEntry *ce, int type)
{ /* actually use the new configuration data */
ConfigEntry *cep, *cep2;
struct jwt_service **ss = &jwt_services;
if (*ss)
ss = &((*ss)->next);
if (type != CONFIG_MAIN)
return 0;
if (!ce || strcmp(ce->name, MYCONF))
return 0;
for (cep = ce->items; cep; cep = cep->next)
{
if (!strcmp(cep->name, "method"))
{
cfg.method = EXTJWT_METHOD_from_string(cep->value);
continue;
}
if (!strcmp(cep->name, "expire-after"))
{
cfg.exp_delay = atoi(cep->value);
continue;
}
if (!strcmp(cep->name, "secret"))
{
cfg.secret = strdup(cep->value);
continue;
}
if (!strcmp(cep->name, "key"))
{
cfg.secret = extjwt_read_file_contents(cep->value, 0, NULL);
continue;
}
if (!strcmp(cep->name, "verify-url"))
{
cfg.vfy = strdup(cep->value);
continue;
}
if (!strcmp(cep->name, "service"))
{ /* nested block */
*ss = safe_alloc(sizeof(struct jwt_service));
(*ss)->cfg = safe_alloc(sizeof(struct extjwt_config));
safe_strdup((*ss)->name, cep->value); /* copy the service name */
for (cep2 = cep->items; cep2; cep2 = cep2->next)
{
if (!strcmp(cep2->name, "method"))
{
(*ss)->cfg->method = EXTJWT_METHOD_from_string(cep2->value);
continue;
}
if (!strcmp(cep2->name, "expire-after"))
{
(*ss)->cfg->exp_delay = atoi(cep2->value);
continue;
}
if (!strcmp(cep2->name, "secret"))
{
(*ss)->cfg->secret = strdup(cep2->value);
continue;
}
if (!strcmp(cep2->name, "key"))
{
(*ss)->cfg->secret = extjwt_read_file_contents(cep2->value, 0, NULL);
continue;
}
if (!strcmp(cep2->name, "verify-url"))
{
(*ss)->cfg->vfy = strdup(cep2->value);
continue;
}
}
ss = &((*ss)->next);
}
}
return 1;
}
char *extjwt_read_file_contents(const char *file, int absolute, int *size)
{
FILE *f = NULL;
int fsize;
char *filename = NULL;
char *buf = NULL;
do
{
safe_strdup(filename, file);
if (!absolute)
convert_to_absolute_path(&filename, CONFDIR);
f = fopen(filename, "rb");
if (!f)
break;
fseek(f, 0, SEEK_END);
fsize = ftell(f);
fseek(f, 0, SEEK_SET);
buf = safe_alloc(fsize + 1);
fsize = fread(buf, 1, fsize, f);
buf[fsize] = '\0';
if (size)
*size = fsize;
fclose(f);
} while (0);
safe_free(filename);
if (!buf && size)
*size = 0;
return buf;
}
CMD_FUNC(cmd_extjwt)
{
Channel *channel;
char *payload;
char *token, *full_token;
struct jwt_service *service = NULL;
struct extjwt_config *config;
int last = 0;
char message[MAX_TOKEN_CHUNK+1];
if (parc < 2 || BadPtr(parv[1]))
{
sendnumeric(client, ERR_NEEDMOREPARAMS, MSG_EXTJWT);
return;
}
if (parv[1][0] == '*' && parv[1][1] == '\0')
{
channel = NULL; /* not linked to a channel */
} else
{
channel = find_channel(parv[1]);
if (!channel)
{
sendnumeric(client, ERR_NOSUCHNICK, parv[1]);
return;
}
}
if (parc > 2 && !BadPtr(parv[2]))
{
service = find_jwt_service(jwt_services, parv[2]);
if (!service)
{
sendto_one(client, NULL, ":%s FAIL %s NO_SUCH_SERVICE :No such service", me.name, MSG_EXTJWT);
return;
}
}
if (service){
config = service->cfg; /* service config */
} else {
config = &cfg; /* default config */
}
if (!(payload = extjwt_make_payload(client, channel, config)) || !(full_token = extjwt_generate_token(payload, config)))
{
sendto_one(client, NULL, ":%s FAIL %s UNKNOWN_ERROR :Failed to generate token", me.name, MSG_EXTJWT);
return;
}
safe_free(payload);
token = full_token;
do
{
if (strlen(token) <= MAX_TOKEN_CHUNK)
{ /* the remaining data (or whole token) will fit a single irc message */
last = 1;
strcpy(message, token);
} else
{ /* send a chunk and shift buffer */
strlcpy(message, token, MAX_TOKEN_CHUNK+1);
token += MAX_TOKEN_CHUNK;
}
sendto_one(client, NULL, extjwt_message_pattern, me.name, parv[1], "*", last?"":"* ", message);
} while (!last);
safe_free(full_token);
}
char *extjwt_make_payload(Client *client, Channel *channel, struct extjwt_config *config)
{
Membership *lp;
json_t *payload = NULL;
json_t *modes = NULL;
json_t *umodes = NULL;
char *modestring;
char singlemode[2] = { '\0' };
char *result;
if (!IsUser(client))
return NULL;
payload = json_object();
modes = json_array();
umodes = json_array();
json_object_set_new(payload, "exp", json_integer(TStime()+config->exp_delay));
json_object_set_new(payload, "iss", json_string_unreal(me.name));
json_object_set_new(payload, "sub", json_string_unreal(client->name));
json_object_set_new(payload, "account", json_string_unreal(IsLoggedIn(client)?client->user->account:""));
if (config->vfy) /* also add the URL */
json_object_set_new(payload, "vfy", json_string_unreal(config->vfy));
if (IsOper(client)) /* add "o" ircop flag */
json_array_append_new(umodes, json_string("o"));
json_object_set_new(payload, "umodes", umodes);
if (channel)
{ /* fill in channel information and user flags */
lp = find_membership_link(client->user->channel, channel);
if (lp)
{
modestring = lp->member_modes;
while (*modestring)
{
singlemode[0] = *modestring;
json_array_append_new(modes, json_string(singlemode));
modestring++;
}
}
json_object_set_new(payload, "channel", json_string_unreal(channel->name));
json_object_set_new(payload, "joined", json_integer(lp?1:0));
json_object_set_new(payload, "cmodes", modes);
}
result = json_dumps(payload, JSON_COMPACT);
json_decref(modes);
json_decref(umodes);
json_decref(payload);
return result;
}
void b64url(char *b64)
{ /* convert base64 to base64-url */
while (*b64)
{
if (*b64 == '+')
*b64 = '-';
if (*b64 == '/')
*b64 = '_';
if (*b64 == '=')
{
*b64 = '\0';
return;
}
b64++;
}
}
unsigned char *extjwt_hash(int method, const void *key, int keylen, const unsigned char *data, int datalen, unsigned int* resultlen)
{
switch(method)
{
case EXTJWT_METHOD_HS256: case EXTJWT_METHOD_HS384: case EXTJWT_METHOD_HS512:
return extjwt_hmac_extjwt_hash(method, key, keylen, data, datalen, resultlen);
case EXTJWT_METHOD_RS256: case EXTJWT_METHOD_RS384: case EXTJWT_METHOD_RS512: case EXTJWT_METHOD_ES256: case EXTJWT_METHOD_ES384: case EXTJWT_METHOD_ES512:
return extjwt_sha_pem_extjwt_hash(method, key, keylen, data, datalen, resultlen);
}
return NULL;
}
unsigned char* extjwt_sha_pem_extjwt_hash(int method, const void *key, int keylen, const unsigned char *data, int datalen, unsigned int* resultlen)
{
EVP_MD_CTX *mdctx = NULL;
ECDSA_SIG *ec_sig = NULL;
const BIGNUM *ec_sig_r = NULL;
const BIGNUM *ec_sig_s = NULL;
BIO *bufkey = NULL;
const EVP_MD *alg;
int type;
EVP_PKEY *pkey = NULL;
int pkey_type;
unsigned char *sig = NULL;
int ret = 0;
size_t slen;
char *retval = NULL;
char *output = NULL;
char *sig_ptr;
do
{
switch (method)
{
case EXTJWT_METHOD_RS256:
alg = EVP_sha256();
type = EVP_PKEY_RSA;
break;
case EXTJWT_METHOD_RS384:
alg = EVP_sha384();
type = EVP_PKEY_RSA;
break;
case EXTJWT_METHOD_RS512:
alg = EVP_sha512();
type = EVP_PKEY_RSA;
break;
case EXTJWT_METHOD_ES256:
alg = EVP_sha256();
type = EVP_PKEY_EC;
break;
case EXTJWT_METHOD_ES384:
alg = EVP_sha384();
type = EVP_PKEY_EC;
break;
case EXTJWT_METHOD_ES512:
alg = EVP_sha512();
type = EVP_PKEY_EC;
break;
default:
return NULL;
}
#if (OPENSSL_VERSION_NUMBER < 0x10100003L) /* https://github.com/openssl/openssl/commit/8ab31975bacb9c907261088937d3aa4102e3af84 */
if (!(bufkey = BIO_new_mem_buf((void *)key, keylen)))
break; /* out of memory */
#else
if (!(bufkey = BIO_new_mem_buf(key, keylen)))
break; /* out of memory */
#endif
if (!(pkey = PEM_read_bio_PrivateKey(bufkey, NULL, NULL, NULL)))
break; /* invalid key? */
pkey_type = EVP_PKEY_id(pkey);
if (type != pkey_type)
break; /* invalid key type */
if (!(mdctx = EVP_MD_CTX_create()))
break; /* out of memory */
if (EVP_DigestSignInit(mdctx, NULL, alg, NULL, pkey) != 1)
break; /* initialize error */
if (EVP_DigestSignUpdate(mdctx, data, datalen) != 1)
break; /* signing error */
if (EVP_DigestSignFinal(mdctx, NULL, &slen) != 1) /* get required buffer length */
break;
sig = safe_alloc(slen);
if (EVP_DigestSignFinal(mdctx, sig, &slen) != 1)
break;
if (pkey_type != EVP_PKEY_EC)
{
*resultlen = slen;
output = safe_alloc(slen);
memcpy(output, sig, slen);
retval = output;
} else
{
unsigned int degree, bn_len, r_len, s_len, buf_len;
unsigned char *raw_buf = NULL;
EC_KEY *ec_key;
if (!(ec_key = EVP_PKEY_get1_EC_KEY(pkey)))
break; /* out of memory */
degree = EC_GROUP_get_degree(EC_KEY_get0_group(ec_key));
EC_KEY_free(ec_key);
sig_ptr = sig;
if (!(ec_sig = d2i_ECDSA_SIG(NULL, (const unsigned char **)&sig_ptr, slen)))
break; /* out of memory */
ECDSA_SIG_get0(ec_sig, &ec_sig_r, &ec_sig_s);
r_len = BN_num_bytes(ec_sig_r);
s_len = BN_num_bytes(ec_sig_s);
bn_len = (degree+7)/8;
if (r_len>bn_len || s_len > bn_len)
break;
buf_len = bn_len*2;
raw_buf = safe_alloc(buf_len);
BN_bn2bin(ec_sig_r, raw_buf+bn_len-r_len);
BN_bn2bin(ec_sig_s, raw_buf+buf_len-s_len);
output = safe_alloc(buf_len);
*resultlen = buf_len;
memcpy(output, raw_buf, buf_len);
retval = output;
safe_free(raw_buf);
}
} while (0);
if (bufkey)
BIO_free(bufkey);
if (pkey)
EVP_PKEY_free(pkey);
if (mdctx)
EVP_MD_CTX_destroy(mdctx);
if (ec_sig)
ECDSA_SIG_free(ec_sig);
safe_free(sig);
return retval;
}
unsigned char* extjwt_hmac_extjwt_hash(int method, const void *key, int keylen, const unsigned char *data, int datalen, unsigned int* resultlen)
{
const EVP_MD* typ;
char *hmac = safe_alloc(EVP_MAX_MD_SIZE);
switch (method)
{
default:
case EXTJWT_METHOD_HS256:
typ = EVP_sha256();
break;
case EXTJWT_METHOD_HS384:
typ = EVP_sha384();
break;
case EXTJWT_METHOD_HS512:
typ = EVP_sha512();
break;
}
if (HMAC(typ, key, keylen, data, datalen, hmac, resultlen))
{ /* openssl call */
return hmac;
} else {
safe_free(hmac);
return NULL;
}
}
char *extjwt_gen_header(int method)
{ /* returns header json */
json_t *header = NULL;
json_t *alg;
char *result;
header = json_object();
json_object_set_new(header, "typ", json_string("JWT"));
switch (method)
{
default:
case EXTJWT_METHOD_HS256:
alg = json_string("HS256");
break;
case EXTJWT_METHOD_HS384:
alg = json_string("HS384");
break;
case EXTJWT_METHOD_HS512:
alg = json_string("HS512");
break;
case EXTJWT_METHOD_RS256:
alg = json_string("RS256");
break;
case EXTJWT_METHOD_RS384:
alg = json_string("RS384");
break;
case EXTJWT_METHOD_RS512:
alg = json_string("RS512");
break;
case EXTJWT_METHOD_ES256:
alg = json_string("ES256");
break;
case EXTJWT_METHOD_ES384:
alg = json_string("ES384");
break;
case EXTJWT_METHOD_ES512:
alg = json_string("ES512");
break;
case EXTJWT_METHOD_NONE:
alg = json_string("none");
break;
}
json_object_set_new(header, "alg", alg);
result = json_dumps(header, JSON_COMPACT);
json_decref(header);
return result;
}
char *extjwt_generate_token(const char *payload, struct extjwt_config *config)
{
char *header = extjwt_gen_header(config->method);
size_t b64header_size = strlen(header)*4/3 + 8; // base64 has 4/3 overhead
size_t b64payload_size = strlen(payload)*4/3 + 8;
size_t b64sig_size = 4096*4/3 + 8;
size_t b64data_size = b64header_size + b64payload_size + b64sig_size + 4;
char *b64header = safe_alloc(b64header_size);
char *b64payload = safe_alloc(b64payload_size);
char *b64sig = safe_alloc(b64sig_size);
char *b64data = safe_alloc(b64data_size);
unsigned int extjwt_hashsize;
char *extjwt_hash_val = NULL;
char *retval = NULL;
b64_encode(header, strlen(header), b64header, b64header_size);
b64_encode(payload, strlen(payload), b64payload, b64payload_size);
b64url(b64header);
b64url(b64payload);
snprintf(b64data, b64data_size, "%s.%s", b64header, b64payload); // generate first part of the token
if (config->method != EXTJWT_METHOD_NONE)
{
extjwt_hash_val = extjwt_hash(config->method, config->secret, strlen(config->secret), b64data, strlen(b64data), &extjwt_hashsize); // calculate the signature extjwt_hash
if (extjwt_hash_val)
{
b64_encode(extjwt_hash_val, extjwt_hashsize, b64sig, b64sig_size);
b64url(b64sig);
strlcat(b64data, ".", b64data_size); // append signature extjwt_hash to token
strlcat(b64data, b64sig, b64data_size);
retval = b64data;
}
} else
{
retval = b64data;
}
safe_free(header);
safe_free(b64header);
safe_free(b64payload);
safe_free(b64sig);
safe_free(extjwt_hash_val);
if (retval != b64data)
safe_free(b64data);
return retval;
}