/* * 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; }