From f82a242c8fc6b3321eeda56277317bc2f2bc7e5b Mon Sep 17 00:00:00 2001 From: Remi Tricot-Le Breton Date: Thu, 7 May 2026 17:05:16 +0200 Subject: [PATCH] MINOR: jwe: Add option to enable/disable algorithms or encryption algorithms for jwt_decrypt Some users of the jwt_decrypt_XXX converters might want to reject JWT tokens with a specific algorithm or encryption algorithm ("alg" or "enc" field respectively) in order to avoid weak algorithms for instance. This could be done from the configuration but would be tedious. This patch adds the new 'jwt.decrypt_alg_list' and 'jwt.decrypt_enc_list' global options that can be used to define a subset of accepted algorithms --- doc/configuration.txt | 44 +++++++++ src/jwe.c | 225 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 242 insertions(+), 27 deletions(-) diff --git a/doc/configuration.txt b/doc/configuration.txt index de877ab59..ac94b324f 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -1791,6 +1791,8 @@ The following keywords are supported in the "global" section : - insecure-fork-wanted - insecure-setuid-wanted - issuers-chain-path + - jwt.decrypt_alg_list + - jwt.decrypt_enc_list - key-base - limited-quic - localpeer @@ -2888,6 +2890,40 @@ issuers-chain-path The OCSP features are able to use the completed chain when no .issuer was used, or no chain was provided in the PEM. +jwt.decrypt_alg_list + Set the list of algorithms allowed in the jwt_decrypt_XXX converters. JWT + tokens using an unsupported or disabled algorithms will never be decrypted. + The specified algorithms must have the same format as in section 4.1 of + RFC7518 and must be colon-separated. The special "ALL" name can be used to + enable all the supported algorithms (see "jwt_decrypt_jwk" converter for a + complete list) and a '!' can be appended to an algorithm name to explicitly + disable it. + Please note that unless "ALL" is specified, using this option will disable + any algorithm that is not explicitly mentioned in the provided list. + + Examples: + # Enable all algorithms but the "ECDH-ES" one + jwt.decrypt_alg_list ALL:!ECDH-ES + + # Only enable ECDH-ES algorithms + jwt.decrypt_alg_list ECDH-ES:ECDH-ES+A128KW:ECDH-ES+A192KW:ECDH-ES+A256KW + +jwt.decrypt_enc_list + Set the list of encryption algorithms allowed in the jwt_decrypt_XXX + converters. JWT tokens using an unsupported or disabled encryption algorithms + will never be decrypted. + The specified algorithms must have the same format as in section 5.1 of + RFC7518 and must be colon-separated. The special "ALL" name can be used to + enable all the supported algorithms (see "jwt_decrypt_jwk" converter for a + complete list) and a '!' can be appended to an algorithm name to explicitly + disable it. + Please note that unless "ALL" is specified, using this option will disable + any algorithm that is not explicitly mentioned in the provided list. + + Examples: + # Enable only AES GCM encrypting algorithms + jwt.decrypt_enc_list A128GCM:A192GCM:A256GCM + key-base Assigns a default directory to fetch SSL private keys from when a relative path is used with "key" directives. Absolute locations specified prevail and @@ -21897,6 +21933,10 @@ jwt_decrypt_cert() the JOSE header) among the following: RSA1_5, RSA-OAEP, RSA-OAEP-256, ECDH-ES, ECDH-ES+A128KW, ECDH-ES+A192KW or ECDH-ES+A256KW. + The supported algorithms and encryption algorithms ("alg" and "enc" fields of + the JOSE header respectively) can be modified thanks to the + 'jwt.decrypt_alg_list' and 'jwt.decrypt_enc_list' global options. + The JWE token must be provided base64url-encoded and the output will be provided "raw". If an error happens during token parsing, signature verification or content decryption, an empty string will be returned. @@ -21932,6 +21972,10 @@ jwt_decrypt_jwk() so the A128KW, A192KW, ECDH-ES+A128KW and ECDH-ES+A192KW algorithms won't work. + The supported algorithms and encryption algorithms ("alg" and "enc" fields of + the JOSE header respectively) can be modified thanks to the + 'jwt.decrypt_alg_list' and 'jwt.decrypt_enc_list' global options. + The JWE token must be provided base64url-encoded and the output will be provided "raw". If an error happens during token parsing, signature verification or content decryption, an empty string will be returned. diff --git a/src/jwe.c b/src/jwe.c index e36b7b0d8..a022e33ab 100644 --- a/src/jwe.c +++ b/src/jwe.c @@ -26,6 +26,7 @@ struct alg_enc { const char *name; int value; + int enabled; }; /* https://datatracker.ietf.org/doc/html/rfc7518#section-4.1 */ @@ -50,27 +51,34 @@ typedef enum { // JWE_ALG_PBES2_HS512_A256KW, } jwe_alg; -struct alg_enc jwe_algs[] = { - { "RSA1_5", JWE_ALG_RSA1_5 }, - { "RSA-OAEP", JWE_ALG_RSA_OAEP }, - { "RSA-OAEP-256", JWE_ALG_RSA_OAEP_256 }, - { "A128KW", JWE_ALG_A128KW }, - { "A192KW", JWE_ALG_A192KW }, - { "A256KW", JWE_ALG_A256KW }, - { "dir", JWE_ALG_DIR }, - { "ECDH-ES", JWE_ALG_ECDH_ES }, - { "ECDH-ES+A128KW", JWE_ALG_ECDH_ES_A128KW }, - { "ECDH-ES+A192KW", JWE_ALG_ECDH_ES_A192KW }, - { "ECDH-ES+A256KW", JWE_ALG_ECDH_ES_A256KW }, - { "A128GCMKW", JWE_ALG_A128GCMKW }, - { "A192GCMKW", JWE_ALG_A192GCMKW }, - { "A256GCMKW", JWE_ALG_A256GCMKW }, - { "PBES2-HS256+A128KW", JWE_ALG_UNMANAGED }, - { "PBES2-HS384+A192KW", JWE_ALG_UNMANAGED }, - { "PBES2-HS512+A256KW", JWE_ALG_UNMANAGED }, - { NULL, JWE_ALG_UNMANAGED }, +enum { + ALG_ENC_DISABLED = 0, + ALG_ENC_ENABLED = 1 }; +struct alg_enc jwe_algs_dflt[] = { + { "RSA1_5", JWE_ALG_RSA1_5, ALG_ENC_ENABLED }, + { "RSA-OAEP", JWE_ALG_RSA_OAEP, ALG_ENC_ENABLED }, + { "RSA-OAEP-256", JWE_ALG_RSA_OAEP_256, ALG_ENC_ENABLED }, + { "A128KW", JWE_ALG_A128KW, ALG_ENC_ENABLED }, + { "A192KW", JWE_ALG_A192KW, ALG_ENC_ENABLED }, + { "A256KW", JWE_ALG_A256KW, ALG_ENC_ENABLED }, + { "dir", JWE_ALG_DIR, ALG_ENC_ENABLED }, + { "ECDH-ES", JWE_ALG_ECDH_ES, ALG_ENC_ENABLED }, + { "ECDH-ES+A128KW", JWE_ALG_ECDH_ES_A128KW, ALG_ENC_ENABLED }, + { "ECDH-ES+A192KW", JWE_ALG_ECDH_ES_A192KW, ALG_ENC_ENABLED }, + { "ECDH-ES+A256KW", JWE_ALG_ECDH_ES_A256KW, ALG_ENC_ENABLED }, + { "A128GCMKW", JWE_ALG_A128GCMKW, ALG_ENC_ENABLED }, + { "A192GCMKW", JWE_ALG_A192GCMKW, ALG_ENC_ENABLED }, + { "A256GCMKW", JWE_ALG_A256GCMKW, ALG_ENC_ENABLED }, + { "PBES2-HS256+A128KW", JWE_ALG_UNMANAGED, ALG_ENC_DISABLED }, + { "PBES2-HS384+A192KW", JWE_ALG_UNMANAGED, ALG_ENC_DISABLED }, + { "PBES2-HS512+A256KW", JWE_ALG_UNMANAGED, ALG_ENC_DISABLED }, + { NULL, JWE_ALG_UNMANAGED, ALG_ENC_DISABLED }, +}; + +struct alg_enc *jwe_algs = NULL; + /* https://datatracker.ietf.org/doc/html/rfc7518#section-5.1 */ typedef enum { JWE_ENC_UNMANAGED = -1, @@ -82,16 +90,18 @@ typedef enum { JWE_ENC_A256GCM, } jwe_enc; -struct alg_enc jwe_encodings[] = { - { "A128CBC-HS256", JWE_ENC_A128CBC_HS256 }, - { "A192CBC-HS384", JWE_ENC_A192CBC_HS384 }, - { "A256CBC-HS512", JWE_ENC_A256CBC_HS512 }, - { "A128GCM", JWE_ENC_A128GCM }, - { "A192GCM", JWE_ENC_A192GCM }, - { "A256GCM", JWE_ENC_A256GCM }, - { NULL, JWE_ENC_UNMANAGED }, +struct alg_enc jwe_encodings_dflt[] = { + { "A128CBC-HS256", JWE_ENC_A128CBC_HS256, ALG_ENC_ENABLED }, + { "A192CBC-HS384", JWE_ENC_A192CBC_HS384, ALG_ENC_ENABLED }, + { "A256CBC-HS512", JWE_ENC_A256CBC_HS512, ALG_ENC_ENABLED }, + { "A128GCM", JWE_ENC_A128GCM, ALG_ENC_ENABLED }, + { "A192GCM", JWE_ENC_A192GCM, ALG_ENC_ENABLED }, + { "A256GCM", JWE_ENC_A256GCM, ALG_ENC_ENABLED }, + { NULL, JWE_ENC_UNMANAGED, ALG_ENC_DISABLED }, }; +struct alg_enc *jwe_encodings = NULL; + /* * In the JWE Compact Serialization, a JWE is represented as the concatenation: @@ -131,6 +141,8 @@ static inline int parse_alg_enc(struct buffer *buf, struct alg_enc *array) while (item->name) { if (strncmp(item->name, b_orig(buf), (int)b_data(buf)) == 0) { + if (item->enabled == ALG_ENC_DISABLED) + return -1; val = item->value; break; } @@ -2059,6 +2071,165 @@ end: } +/* + * Duplicate algorithm and encoding arrays so that their enabled flags + * can be overwritten (when using the jwt.encrypt_alg/enc_list options). + * Return 0 in case of success, -1 otherwise. + */ +static int dup_alg_enc_arrays(void) +{ + int ret = -1; + + if (!jwe_algs) { + jwe_algs = malloc(sizeof(jwe_algs_dflt)); + if (!jwe_algs) { + ha_alert("Unable to allocate the JWE algorithms array.\n"); + goto end; + } + memcpy(jwe_algs, jwe_algs_dflt, sizeof(jwe_algs_dflt)); + } + + if (!jwe_encodings) { + jwe_encodings = malloc(sizeof(jwe_encodings_dflt)); + if (!jwe_encodings) { + ha_alert("Unable to allocate the JWE encodings array.\n"); + goto end; + } + memcpy(jwe_encodings, jwe_encodings_dflt, sizeof(jwe_encodings_dflt)); + } + + ret = 0; + +end: + return ret; +} + +static int jwe_init(void) +{ + /* Duplicate algorithm and encoding arrays so that their enabled flags + * can be overwritten */ + return dup_alg_enc_arrays(); +} + +REGISTER_PRE_CHECK(jwe_init); + +static void jwe_deinit(void) +{ + ha_free(&jwe_algs); + ha_free(&jwe_encodings); +} + +REGISTER_POST_DEINIT(jwe_deinit); + +/* + * Look for an entry named in array and set its 'enabled' field + * to . + * Return 0 in case of success, 1 otherwise (item not found). + */ +static int set_alg_enc_flag(struct alg_enc *arr, char *algname, int algname_len, int value) +{ + struct alg_enc *item = &arr[0]; + + while (item->name) { + if (algname_len == strlen(item->name) && strncmp(algname, item->name, algname_len) == 0) { + item->enabled = value; + return 0; + } + ++item; + } + + return 1; +} + +/* Set the 'enabled' field to for all members of . */ +static void set_all_alg_enc_flags(struct alg_enc *arr, int val) +{ + struct alg_enc *item = &arr[0]; + + while (item->name) { + item->enabled = val; + ++item; + } +} + +/* + * Parse algorithm list comprised of colon-separated algorithms having the same + * format as in jwe_algs_dflt or jwe_encodings_dflt arrays. + * When the option is used, all the algorithms are first disabled so any + * unspecified algo will not be enabled anymore. + */ +static int jwe_parse_global_alg_enc_list(char **args, int section_type, struct proxy *curpx, + const struct proxy *defpx, const char *file, int line, + char **err) +{ + struct alg_enc *arr = NULL; + char *p, *arg = args[1]; + char *arg_end = arg+strlen(arg); + char *alg, *alg_end; + + int value = ALG_ENC_ENABLED; + int ret = -1; + + if (dup_alg_enc_arrays()) + goto end; + + if (args[0][14] == 'a') { + /* "jwe.supported_algorithms" */ + arr = jwe_algs; + } else { + /* "jwe.supported_encodings" */ + arr = jwe_encodings; + } + + /* Disable all algorithms/encodings */ + set_all_alg_enc_flags(arr, ALG_ENC_DISABLED); + + while (*arg) { + value = ALG_ENC_ENABLED; + + if (*arg == '!') { + ++arg; + value = ALG_ENC_DISABLED; + } + alg = arg; + alg_end = NULL; + + p = strchr(arg, ':'); + if (p) { + alg_end = p; + arg = p + 1; + } else { + alg_end = arg_end; + arg = arg_end; + } + + /* If "ALL" is used all previously disabled algo will be + * reenabled */ + if (strncmp(alg, "ALL", alg_end - alg) == 0) { + set_all_alg_enc_flags(arr, ALG_ENC_ENABLED); + } else { + if (set_alg_enc_flag(arr, alg, alg_end-alg, value) != 0) { + memprintf(err, "Unknown algorithm/encoding %.*s\n", (int)(alg_end-alg), alg); + goto end; + } + } + } + + ret = 0; +end: + return ret; + +} + +static struct cfg_kw_list cfg_kws = {ILH, { + { CFG_GLOBAL, "jwt.decrypt_alg_list", jwe_parse_global_alg_enc_list }, + { CFG_GLOBAL, "jwt.decrypt_enc_list", jwe_parse_global_alg_enc_list }, + { 0, NULL, NULL }, +}}; + +INITCALL1(STG_REGISTER, cfg_register_keywords, &cfg_kws); + + static struct sample_conv_kw_list sample_conv_kws = {ILH, { /* JSON Web Token converters */ { "jwt_decrypt_secret", sample_conv_jwt_decrypt_secret, ARG1(1,STR), sample_conv_jwt_decrypt_secret_check, SMP_T_BIN, SMP_T_BIN },