mirror of
https://github.com/unrealircd/unrealircd.git
synced 2026-07-03 20:03:12 +02:00
4cf2940605
Ah okay, the `continue` in the switch was used as a `break 2`. Changed to a `return` now as no memory is allocated anyway and nothing further needs to be done. Also makes it immediately clear (if you read the code) that processing ends there.
1153 lines
31 KiB
C
1153 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 (!in && !*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)
|
|
{
|
|
return 1; /* TODO enable */
|
|
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;
|
|
}
|