From 93691313f3873dca681b82c7f402ee4467894dad Mon Sep 17 00:00:00 2001 From: Daniel-Constantin Mierla Date: Wed, 10 Mar 2021 16:15:01 +0100 Subject: [PATCH] jwt: new module adding json web token generation and validation --- src/modules/jwt/Makefile | 25 ++ src/modules/jwt/README | 176 ++++++++++++ src/modules/jwt/doc/Makefile | 4 + src/modules/jwt/doc/jwt.xml | 37 +++ src/modules/jwt/doc/jwt_admin.xml | 218 +++++++++++++++ src/modules/jwt/jwt_mod.c | 442 ++++++++++++++++++++++++++++++ 6 files changed, 902 insertions(+) create mode 100644 src/modules/jwt/Makefile create mode 100644 src/modules/jwt/README create mode 100644 src/modules/jwt/doc/Makefile create mode 100644 src/modules/jwt/doc/jwt.xml create mode 100644 src/modules/jwt/doc/jwt_admin.xml create mode 100644 src/modules/jwt/jwt_mod.c diff --git a/src/modules/jwt/Makefile b/src/modules/jwt/Makefile new file mode 100644 index 00000000000..0d4f553cd81 --- /dev/null +++ b/src/modules/jwt/Makefile @@ -0,0 +1,25 @@ +# +# +# WARNING: do not run this directly, it should be run by the main Makefile + +include ../../Makefile.defs +auto_gen= +NAME=jwt.so + +ifeq ($(CROSS_COMPILE),) +JWT_BUILDER=$(shell \ + if pkg-config --exists libjwt; then \ + echo 'pkg-config libjwt'; \ + fi) +endif + +ifneq ($(JWT_BUILDER),) + DEFS += $(shell $(JWT_BUILDER) --cflags) + LIBS += $(shell $(JWT_BUILDER) --libs) +else + DEFS += -I$(LOCALBASE)/include + LIBS += -L$(LOCALBASE)/lib -ljwt +endif + +include ../../Makefile.modules + diff --git a/src/modules/jwt/README b/src/modules/jwt/README new file mode 100644 index 00000000000..cc9c7050313 --- /dev/null +++ b/src/modules/jwt/README @@ -0,0 +1,176 @@ +JWT Module + +Daniel-Constantin Mierla + + + +Edited by + +Daniel-Constantin Mierla + + + + Copyright © 2021 asipto.com + __________________________________________________________________ + + Table of Contents + + 1. Admin Guide + + 1. Overview + 2. Dependencies + + 2.1. Kamailio Modules + 2.2. External Libraries or Applications + + 3. Parameters + + 3.1. key_mode (int) + + 4. Functions + + 4.1. jwt_generate(prvkey, alg, claims) + 4.2. jwt_verify(pubkey, alg, claims, jwtval) + + 5. Variables + + 5.1. $jwt(key) + + List of Examples + + 1.1. Set key_mode parameter + 1.2. jwt_generate usage + 1.3. jwt_verify usage + 1.4. $jwt(name) usage + +Chapter 1. Admin Guide + + Table of Contents + + 1. Overview + 2. Dependencies + + 2.1. Kamailio Modules + 2.2. External Libraries or Applications + + 3. Parameters + + 3.1. key_mode (int) + + 4. Functions + + 4.1. jwt_generate(prvkey, alg, claims) + 4.2. jwt_verify(pubkey, alg, claims, jwtval) + + 5. Variables + + 5.1. $jwt(key) + +1. Overview + + This module provides JWT (JSON Web Token) functions to be used in + Kamailio configuration file. + + It relies on libjwt (at least v1.12.0) library + (https://github.com/benmcollins/libjwt). + +2. Dependencies + + 2.1. Kamailio Modules + 2.2. External Libraries or Applications + +2.1. Kamailio Modules + + The following modules must be loaded before this module: + * none. + +2.2. External Libraries or Applications + + The following libraries or applications must be installed before + running Kamailio with this module loaded: + * libjwt - minimum version 1.12.0. + +3. Parameters + + 3.1. key_mode (int) + +3.1. key_mode (int) + + Mode to store the private and public keys. + + Work in progress. + + Default value is 0. + + Example 1.1. Set key_mode parameter +... +modparam("jwt", "key_mode", 0) +... + +4. Functions + + 4.1. jwt_generate(prvkey, alg, claims) + 4.2. jwt_verify(pubkey, alg, claims, jwtval) + +4.1. jwt_generate(prvkey, alg, claims) + + Generate the JWT, its value can be retrieved in the variable $jwt(val). + + The parameters are: + * prvkey - path to private key + * alg - the algoritm to build the signature, as supported by the + libjwt (e.g., RS256, HS256, ES256, ...) + * claims - the list of claims to be added to JWT, in the format + "name1=value1;name2=value2;..." (same as the SIP parameters + format). + + This function can be used from ANY_ROUTE. + + Example 1.2. jwt_generate usage +... + jwt_generate("/path/to/prvkey.pem", "RS256", + "caller=$fU;callee=$tU;callid=$ci"); +... + +4.2. jwt_verify(pubkey, alg, claims, jwtval) + + Verify the JWT. + + The parameters are: + * pubkey - path to public key + * alg - the algoritm to build the signature, as supported by the + libjwt (e.g., RS256, HS256, ES256, ...) + * claims - the list of claims to be checked they are in the JWT, in + the format "name1=value1;name2=value2;..." (same as the SIP + parameters format). + * jwtval - the value of the JWT to verify + + This function can be used from ANY_ROUTE. + + Example 1.3. jwt_verify usage +... + if(!jwt_verify("/path/to/pubkey.pem", "RS256", + "caller=$fU;callee=$tU;callid=$ci", + "$var(jwt)") { + xwarn("failed to verify jwt\n"); + } +... + +5. Variables + + 5.1. $jwt(key) + +5.1. $jwt(key) + + Get the values and attributes after using JWT functions. + + The key can be: + * val - the value of JWT after a successful jwt_generate(). + * status - the status of verification after a failed jwt_verify(). + + Example 1.4. $jwt(name) usage +... + jwt_generate("/path/to/prvkey.pem", "RS256", + "caller=$fU;callee=$tU;callid=$ci"); + xinfo("jwt is: $jwt(val)"); +... diff --git a/src/modules/jwt/doc/Makefile b/src/modules/jwt/doc/Makefile new file mode 100644 index 00000000000..4b4bd6abff9 --- /dev/null +++ b/src/modules/jwt/doc/Makefile @@ -0,0 +1,4 @@ +docs = jwt.xml + +docbook_dir = ../../../../doc/docbook +include $(docbook_dir)/Makefile.module diff --git a/src/modules/jwt/doc/jwt.xml b/src/modules/jwt/doc/jwt.xml new file mode 100644 index 00000000000..62225bb855f --- /dev/null +++ b/src/modules/jwt/doc/jwt.xml @@ -0,0 +1,37 @@ + + + +%docentities; + +]> + + + + JWT Module + kamailio.org + + + Daniel-Constantin + Mierla + miconda@gmail.com + + + Daniel-Constantin + Mierla + miconda@gmail.com + + + + 2021 + asipto.com + + + + + + + + diff --git a/src/modules/jwt/doc/jwt_admin.xml b/src/modules/jwt/doc/jwt_admin.xml new file mode 100644 index 00000000000..e222e81dc73 --- /dev/null +++ b/src/modules/jwt/doc/jwt_admin.xml @@ -0,0 +1,218 @@ + + + +%docentities; + +]> + + + + + &adminguide; + +
+ Overview + + This module provides JWT (JSON Web Token) functions to be used + in &kamailio; configuration file. + + + It relies on libjwt (at least v1.12.0) library (https://github.com/benmcollins/libjwt). + +
+ +
+ Dependencies +
+ &kamailio; Modules + + The following modules must be loaded before this module: + + + + none. + + + + +
+
+ External Libraries or Applications + + The following libraries or applications must be installed before running + &kamailio; with this module loaded: + + + + libjwt - minimum version 1.12.0. + + + + +
+
+ +
+ Parameters +
+ <varname>key_mode</varname> (int) + + Mode to store the private and public keys. + + + Work in progress. + + + + Default value is 0. + + + + Set <varname>key_mode</varname> parameter + +... +modparam("jwt", "key_mode", 0) +... + + +
+ +
+ +
+ Functions +
+ + <function moreinfo="none">jwt_generate(prvkey, alg, claims)</function> + + + Generate the JWT, its value can be retrieved in the variable $jwt(val). + + + The parameters are: + + + + + prvkey - path to private key + + + + + alg - the algoritm to build the signature, as supported by the + libjwt (e.g., RS256, HS256, ES256, ...) + + + + + claims - the list of claims to be added to JWT, in the format + "name1=value1;name2=value2;..." (same as the SIP parameters format). + + + + + This function can be used from ANY_ROUTE. + + + <function>jwt_generate</function> usage + +... + jwt_generate("/path/to/prvkey.pem", "RS256", + "caller=$fU;callee=$tU;callid=$ci"); +... + + +
+ +
+ + <function moreinfo="none">jwt_verify(pubkey, alg, claims, jwtval)</function> + + + Verify the JWT. + + + The parameters are: + + + + + pubkey - path to public key + + + + + alg - the algoritm to build the signature, as supported by the + libjwt (e.g., RS256, HS256, ES256, ...) + + + + + claims - the list of claims to be checked they are in the JWT, in the format + "name1=value1;name2=value2;..." (same as the SIP parameters format). + + + + + jwtval - the value of the JWT to verify + + + + + This function can be used from ANY_ROUTE. + + + <function>jwt_verify</function> usage + +... + if(!jwt_verify("/path/to/pubkey.pem", "RS256", + "caller=$fU;callee=$tU;callid=$ci", + "$var(jwt)") { + xwarn("failed to verify jwt\n"); + } +... + + +
+
+
+ Variables +
+ + <function moreinfo="none">$jwt(key)</function> + + + Get the values and attributes after using JWT functions. + + + The key can be: + + + + + val - the value of JWT after a successful jwt_generate(). + + + + + status - the status of verification after a failed jwt_verify(). + + + + + <function>$jwt(name)</function> usage + +... + jwt_generate("/path/to/prvkey.pem", "RS256", + "caller=$fU;callee=$tU;callid=$ci"); + xinfo("jwt is: $jwt(val)"); +... + + +
+
+ +
diff --git a/src/modules/jwt/jwt_mod.c b/src/modules/jwt/jwt_mod.c new file mode 100644 index 00000000000..25b32b1bd0a --- /dev/null +++ b/src/modules/jwt/jwt_mod.c @@ -0,0 +1,442 @@ +/** + * Copyright (C) 2021 Daniel-Constantin Mierla (asipto.com) + * + * This file is part of Kamailio, a free SIP server. + * + * This file 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 2 of the License, or + * (at your option) any later version + * + * + * This file 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include +#include +#include +#include + +#include + +#include "../../core/sr_module.h" +#include "../../core/dprint.h" +#include "../../core/mod_fix.h" +#include "../../core/lvalue.h" +#include "../../core/kemi.h" +#include "../../core/parser/parse_param.h" + + +MODULE_VERSION + +static int mod_init(void); +static int child_init(int); +static void mod_destroy(void); + +static int w_jwt_generate(sip_msg_t* msg, char* pkey, char* palg, char* pclaims); +static int w_jwt_verify(sip_msg_t* msg, char* pkey, char* palg, char* pclaims, + char *pjwtval); + +static int _jwt_key_mode = 0; + +static str _jwt_result = STR_NULL; +static unsigned int _jwt_verify_status = 0; + +static cmd_export_t cmds[]={ + {"jwt_generate", (cmd_function)w_jwt_generate, 3, + fixup_spve_all, 0, ANY_ROUTE}, + {"jwt_verify", (cmd_function)w_jwt_verify, 4, + fixup_spve_all, 0, ANY_ROUTE}, + {0, 0, 0, 0, 0, 0} +}; + +static param_export_t params[]={ + { "key_mode", PARAM_INT, &_jwt_key_mode }, + + { 0, 0, 0 } +}; + +static int jwt_pv_get(sip_msg_t *msg, pv_param_t *param, pv_value_t *res); +static int jwt_pv_parse_name(pv_spec_t *sp, str *in); +static pv_export_t mod_pvs[] = { + { {"jwt", sizeof("jwt")-1}, PVT_OTHER, jwt_pv_get, 0, + jwt_pv_parse_name, 0, 0, 0 }, + { {0, 0}, 0, 0, 0, 0, 0, 0, 0 } +}; + +struct module_exports exports = { + "jwt", /* module name */ + DEFAULT_DLFLAGS, /* dlopen flags */ + cmds, /* cmd (cfg function) exports */ + params, /* param exports */ + 0, /* RPC method exports */ + mod_pvs, /* pseudo-variables exports */ + 0, /* response handling function */ + mod_init, /* module init function */ + child_init, /* per-child init function */ + mod_destroy /* module destroy function */ +}; + + +/** + * @brief Initialize crypto module function + */ +static int mod_init(void) +{ + return 0; +} + +/** + * @brief Initialize crypto module children + */ +static int child_init(int rank) +{ + return 0; +} + +/** + * destroy module function + */ +static void mod_destroy(void) +{ + return; +} + +/** + * + */ +static int ki_jwt_generate(sip_msg_t* msg, str *key, str *alg, str *claims) +{ + str dupclaims = STR_NULL; + str sparams = STR_NULL; + jwt_alg_t valg = JWT_ALG_NONE; + time_t iat; + FILE *fpk = NULL; + unsigned char keybuf[10240]; + size_t keybuf_len = 0; + param_t* params_list = NULL; + param_hooks_t phooks; + param_t *pit = NULL; + int ret = 0; + jwt_t *jwt = NULL; + + if(key==NULL || key->s==NULL || alg==NULL || alg->s==NULL + || claims==NULL || claims->s==NULL || claims->len<=0) { + LM_ERR("invalid parameters\n"); + return -1; + } + if(_jwt_result.s != NULL) { + jwt_free_str(_jwt_result.s); + _jwt_result.s = NULL; + _jwt_result.len = 0; + } + valg = jwt_str_alg(alg->s); + if (valg == JWT_ALG_INVAL) { + LM_ERR("not supported algorithm: %s\n", alg->s); + return -1; + } + if(pkg_str_dup(&dupclaims, claims)<0) { + LM_ERR("failed to duplicate claims\n"); + return -1; + } + fpk= fopen(key->s, "r"); + if(fpk==NULL) { + LM_ERR("failed to read key file: %s\n", key->s); + goto error; + } + keybuf_len = fread(keybuf, 1, sizeof(keybuf), fpk); + fclose(fpk); + if(keybuf_len==0) { + LM_ERR("unable to read key file content: %s\n", key->s); + goto error; + } + keybuf[keybuf_len] = '\0'; + sparams = dupclaims; + if(sparams.s[sparams.len-1]==';') { + sparams.len--; + } + if (parse_params(&sparams, CLASS_ANY, &phooks, ¶ms_list)<0) { + LM_ERR("failed to parse claims\n"); + goto error; + } + + ret = jwt_new(&jwt); + if (ret != 0 || jwt == NULL) { + LM_ERR("failed to initialize jwt\n"); + goto error; + } + + iat = time(NULL); + + ret = jwt_add_grant_int(jwt, "iat", iat); + for (pit = params_list; pit; pit=pit->next) { + if(pit->name.len>0 && pit->body.len>0) { + pit->name.s[pit->name.len] = '\0'; + pit->body.s[pit->body.len] = '\0'; + jwt_add_grant(jwt, pit->name.s, pit->body.s); + } + } + + ret = jwt_set_alg(jwt, valg, keybuf, keybuf_len); + if (ret < 0) { + LM_ERR("failed to set algorithm and key\n"); + goto error; + } + + _jwt_result.s = jwt_encode_str(jwt); + _jwt_result.len = strlen(_jwt_result.s); + + free_params(params_list); + pkg_free(dupclaims.s); + jwt_free(jwt); + + return 1; + +error: + if(params_list!=NULL) { + free_params(params_list); + } + if(dupclaims.s!=NULL) { + pkg_free(dupclaims.s); + } + if(jwt!=NULL) { + jwt_free(jwt); + } + return -1; +} + +/** + * + */ +static int w_jwt_generate(sip_msg_t* msg, char* pkey, char* palg, char* pclaims) +{ + str skey = STR_NULL; + str salg = STR_NULL; + str sclaims = STR_NULL; + + if (fixup_get_svalue(msg, (gparam_t*)pkey, &skey) != 0) { + LM_ERR("cannot get path to the key file\n"); + return -1; + } + if (fixup_get_svalue(msg, (gparam_t*)palg, &salg) != 0) { + LM_ERR("cannot get algorithm value\n"); + return -1; + } + + if (fixup_get_svalue(msg, (gparam_t*)pclaims, &sclaims) != 0) { + LM_ERR("cannot get claims value\n"); + return -1; + } + + return ki_jwt_generate(msg, &skey, &salg, &sclaims); +} + +/** + * + */ +static int ki_jwt_verify(sip_msg_t* msg, str *key, str *alg, str *claims, + str *jwtval) +{ + str dupclaims = STR_NULL; + jwt_alg_t valg = JWT_ALG_NONE; + time_t iat; + FILE *fpk = NULL; + unsigned char keybuf[10240]; + size_t keybuf_len = 0; + param_t* params_list = NULL; + param_hooks_t phooks; + param_t *pit = NULL; + int ret = 0; + jwt_t *jwt = NULL; + jwt_valid_t *jwt_valid = NULL; + str sparams = STR_NULL; + + if(key==NULL || key->s==NULL || alg==NULL || alg->s==NULL + || claims==NULL || claims->s==NULL || claims->len<=0 + || jwtval==NULL || jwtval->s==NULL || jwtval->len<=0) { + LM_ERR("invalid parameters\n"); + return -1; + } + + _jwt_verify_status = 0; + + valg = jwt_str_alg(alg->s); + if (valg == JWT_ALG_INVAL) { + LM_ERR("not supported algorithm: %s\n", alg->s); + return -1; + } + if(pkg_str_dup(&dupclaims, claims)<0) { + LM_ERR("failed to duplicate claims\n"); + return -1; + } + fpk= fopen(key->s, "r"); + if(fpk==NULL) { + LM_ERR("failed to read key file: %s\n", key->s); + goto error; + } + keybuf_len = fread(keybuf, 1, sizeof(keybuf), fpk); + fclose(fpk); + if(keybuf_len==0) { + LM_ERR("unable to read key file content: %s\n", key->s); + goto error; + } + keybuf[keybuf_len] = '\0'; + sparams = dupclaims; + if(sparams.s[sparams.len-1]==';') { + sparams.len--; + } + if (parse_params(&sparams, CLASS_ANY, &phooks, ¶ms_list)<0) { + LM_ERR("failed to parse claims\n"); + goto error; + } + + ret = jwt_valid_new(&jwt_valid, valg); + if (ret != 0 || jwt_valid == NULL) { + LM_ERR("failed to initialize jwt valid\n"); + goto error; + } + + iat = time(NULL); + jwt_valid_set_headers(jwt_valid, 1); + jwt_valid_set_now(jwt_valid, iat); + + for (pit = params_list; pit; pit=pit->next) { + if(pit->name.len>0 && pit->body.len>0) { + pit->name.s[pit->name.len] = '\0'; + pit->body.s[pit->body.len] = '\0'; + jwt_valid_add_grant(jwt_valid, pit->name.s, pit->body.s); + } + } + + ret = jwt_decode(&jwt, jwtval->s, keybuf, keybuf_len); + if (ret!=0 || jwt==NULL) { + LM_ERR("failed to decode jwt value\n"); + goto error; + } + if (jwt_validate(jwt, jwt_valid) != 0) { + _jwt_verify_status = jwt_valid_get_status(jwt_valid); + LM_ERR("failed to validate jwt: %08x\n", _jwt_verify_status); + goto error; + } + + free_params(params_list); + pkg_free(dupclaims.s); + jwt_free(jwt); + jwt_valid_free(jwt_valid); + + return 1; + +error: + if(params_list!=NULL) { + free_params(params_list); + } + if(dupclaims.s!=NULL) { + pkg_free(dupclaims.s); + } + if(jwt!=NULL) { + jwt_free(jwt); + } + if(jwt_valid!=NULL) { + jwt_valid_free(jwt_valid); + } + return -1; +} + +/** + * + */ +static int w_jwt_verify(sip_msg_t* msg, char* pkey, char* palg, char* pclaims, + char *pjwtval) +{ + str skey = STR_NULL; + str salg = STR_NULL; + str sclaims = STR_NULL; + str sjwtval = STR_NULL; + + if (fixup_get_svalue(msg, (gparam_t*)pkey, &skey) != 0) { + LM_ERR("cannot get path to the key file\n"); + return -1; + } + if (fixup_get_svalue(msg, (gparam_t*)palg, &salg) != 0) { + LM_ERR("cannot get algorithm value\n"); + return -1; + } + if (fixup_get_svalue(msg, (gparam_t*)pclaims, &sclaims) != 0) { + LM_ERR("cannot get claims value\n"); + return -1; + } + if (fixup_get_svalue(msg, (gparam_t*)pjwtval, &sjwtval) != 0) { + LM_ERR("cannot get jwt value\n"); + return -1; + } + + return ki_jwt_verify(msg, &skey, &salg, &sclaims, &sjwtval); +} + +/** + * + */ +static int jwt_pv_get(sip_msg_t *msg, pv_param_t *param, pv_value_t *res) +{ + switch(param->pvn.u.isname.name.n) + { + case 0: + if(_jwt_result.s==NULL) + return pv_get_null(msg, param, res); + return pv_get_strval(msg, param, res, &_jwt_result); + case 1: + return pv_get_uintval(msg, param, res, _jwt_verify_status); + default: + return pv_get_null(msg, param, res); + } +} + +/** + * + */ +static int jwt_pv_parse_name(pv_spec_t *sp, str *in) +{ + if(in->len==3 && strncmp(in->s, "val", 3)==0) { + sp->pvp.pvn.u.isname.name.n = 0; + } else if(in->len==6 && strncmp(in->s, "status", 6)==0) { + sp->pvp.pvn.u.isname.name.n = 1; + } else { + LM_ERR("unknown inner name [%.*s]\n", in->len, in->s); + return -1; + } + return 0; +} + +/** + * + */ +/* clang-format off */ +static sr_kemi_t sr_kemi_jwt_exports[] = { + { str_init("jwt"), str_init("jwt_generate"), + SR_KEMIP_INT, ki_jwt_generate, + { SR_KEMIP_STR, SR_KEMIP_STR, SR_KEMIP_STR, + SR_KEMIP_NONE, SR_KEMIP_NONE, SR_KEMIP_NONE } + }, + { str_init("jwt"), str_init("jwt_verify"), + SR_KEMIP_INT, ki_jwt_verify, + { SR_KEMIP_STR, SR_KEMIP_STR, SR_KEMIP_STR, + SR_KEMIP_STR, SR_KEMIP_NONE, SR_KEMIP_NONE } + }, + + { {0, 0}, {0, 0}, 0, NULL, { 0, 0, 0, 0, 0, 0 } } +}; +/* clang-format on */ + +int mod_register(char *path, int *dlflags, void *p1, void *p2) +{ + sr_kemi_modules_add(sr_kemi_jwt_exports); + return 0; +}