mirror of https://github.com/pissnet/pissircd.git
1152 lines
31 KiB
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;
|
|
}
|