From: Remi Tricot-Le Breton Date: Tue, 13 Jan 2026 10:50:57 +0000 (+0100) Subject: MINOR: jwe: Add new jwt_decrypt_cert converter X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e3a782adb59f5d8573e415c4f436f48ddc300736;p=thirdparty%2Fhaproxy.git MINOR: jwe: Add new jwt_decrypt_cert converter This converter checks the validity and decrypts the content of a JWE token that has an asymetric "alg" algorithm (RSA). In such a case, we must provide a path to an already loaded certificate and private key that has the "jwt" option set to "on". --- diff --git a/src/jwe.c b/src/jwe.c index eb16e7f19..095181ecd 100644 --- a/src/jwe.c +++ b/src/jwe.c @@ -31,9 +31,9 @@ struct alg_enc { /* https://datatracker.ietf.org/doc/html/rfc7518#section-4.1 */ typedef enum { JWE_ALG_UNMANAGED = -1, - // JWE_ALG_RSA1_5, - // JWE_ALG_RSA_OAEP, - // JWE_ALG_RSA_OAEP_256, + JWE_ALG_RSA1_5, + JWE_ALG_RSA_OAEP, + JWE_ALG_RSA_OAEP_256, JWE_ALG_A128KW, JWE_ALG_A192KW, JWE_ALG_A256KW, @@ -51,9 +51,9 @@ typedef enum { } jwe_alg; struct alg_enc jwe_algs[] = { - { "RSA1_5", JWE_ALG_UNMANAGED }, - { "RSA-OAEP", JWE_ALG_UNMANAGED }, - { "RSA-OAEP-256", JWE_ALG_UNMANAGED }, + { "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 }, @@ -504,8 +504,8 @@ static int sample_conv_jwt_decrypt_secret(const struct arg *args, struct sample struct buffer **cek = NULL; struct buffer *decrypted_cek = NULL; struct buffer *out = NULL; - struct buffer *alg_iv = NULL; struct buffer *alg_tag = NULL; + struct buffer *alg_iv = NULL; int size = 0; jwe_alg alg = JWE_ALG_UNMANAGED; jwe_enc enc = JWE_ENC_UNMANAGED; @@ -618,7 +618,6 @@ static int sample_conv_jwt_decrypt_secret(const struct arg *args, struct sample chunk_memcpy(decrypted_cek, secret_smp.data.u.str.area, secret_smp.data.u.str.data); } - /* Decode the encrypted content thanks to decrypted_cek secret */ if (decrypt_ciphertext(enc, items, decoded_items, decrypted_cek, &out)) goto end; @@ -641,6 +640,230 @@ end: } +static int decrypt_cek_rsa(struct buffer *cek, struct buffer *decrypted_cek, + struct buffer *cert, jwe_alg crypt_alg) +{ + EVP_PKEY_CTX *ctx = NULL; + const EVP_MD *md = NULL; + EVP_PKEY *pkey = NULL; + int retval = 0; + int pad = 0; + size_t outl = b_size(decrypted_cek); + + struct ckch_store *store = NULL; + + if (HA_SPIN_TRYLOCK(CKCH_LOCK, &ckch_lock)) + goto end; + + store = ckchs_lookup(b_orig(cert)); + if (!store || !store->data->key || !store->conf.jwt) { + HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock); + goto end; + } + + pkey = store->data->key; + + EVP_PKEY_up_ref(pkey); + + HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock); + + switch(crypt_alg) { + case JWE_ALG_RSA1_5: + pad = RSA_PKCS1_PADDING; + md = EVP_sha1(); + break; + case JWE_ALG_RSA_OAEP: + pad = RSA_PKCS1_OAEP_PADDING; + md = EVP_sha1(); + break; + case JWE_ALG_RSA_OAEP_256: + pad = RSA_PKCS1_OAEP_PADDING; + md = EVP_sha256(); + break; + default: + goto end; + } + + ctx = EVP_PKEY_CTX_new(pkey, NULL); + + if (!ctx) + goto end; + + if (EVP_PKEY_decrypt_init(ctx) <= 0) + goto end; + + if (EVP_PKEY_CTX_set_rsa_padding(ctx, pad) <= 0) + goto end; + + if (pad == RSA_PKCS1_OAEP_PADDING) { + if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx, md) <= 0) + goto end; + + if (EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, md) <= 0) + goto end; + } + + if (EVP_PKEY_decrypt(ctx, (unsigned char*)b_orig(decrypted_cek), &outl, + (unsigned char*)b_orig(cek), b_data(cek)) <= 0) + goto end; + + decrypted_cek->data = outl; + + retval = 1; + +end: + EVP_PKEY_CTX_free(ctx); + EVP_PKEY_free(pkey); + return retval; +} + + +/* + * Decrypt the contents of a JWE token thanks to the user-provided certificate + * and private key. This converter can only be used for tokens that have an + * asymetric algorithm (RSA only for now). + * Returns the decrypted contents, or nothing if any error happened. + */ +static int sample_conv_jwt_decrypt_cert(const struct arg *args, struct sample *smp, void *private) +{ + struct sample cert_smp; + struct buffer *input = NULL; + unsigned int item_num = JWE_ELT_MAX; + int retval = 0; + struct jwt_item items[JWE_ELT_MAX] = {}; + struct buffer *decoded_items[JWE_ELT_MAX] = {}; + jwe_alg alg = JWE_ALG_UNMANAGED; + jwe_enc enc = JWE_ENC_UNMANAGED; + int rsa = 0; + int size = 0; + struct buffer *cert = NULL; + struct buffer **cek = NULL; + struct buffer *decrypted_cek = NULL; + struct buffer *out = NULL; + struct jose_fields fields = {}; + + input = alloc_trash_chunk(); + if (!input) + return 0; + + if (!chunk_cpy(input, &smp->data.u.str)) + goto end; + + if (jwt_tokenize(input, items, &item_num) || item_num != JWE_ELT_MAX) + goto end; + + /* Base64Url decode the JOSE header */ + decoded_items[JWE_ELT_JOSE] = alloc_trash_chunk(); + if (!decoded_items[JWE_ELT_JOSE]) + goto end; + size = base64urldec(items[JWE_ELT_JOSE].start, items[JWE_ELT_JOSE].length, + b_orig(decoded_items[JWE_ELT_JOSE]), b_size(decoded_items[JWE_ELT_JOSE])); + if (size < 0) + goto end; + decoded_items[JWE_ELT_JOSE]->data = size; + + if (!parse_jose(decoded_items[JWE_ELT_JOSE], &alg, &enc, &fields)) + goto end; + + /* Check if "alg" fits certificate-based JWEs */ + switch (alg) { + case JWE_ALG_RSA1_5: + case JWE_ALG_RSA_OAEP: + case JWE_ALG_RSA_OAEP_256: + rsa = 1; + break; + default: + /* Not managed yet */ + goto end; + } + + cert = alloc_trash_chunk(); + if (!cert) + goto end; + + smp_set_owner(&cert_smp, smp->px, smp->sess, smp->strm, smp->opt); + if (!sample_conv_var2smp_str(&args[0], &cert_smp)) + goto end; + if (chunk_printf(cert, "%.*s", (int)b_data(&cert_smp.data.u.str), b_orig(&cert_smp.data.u.str)) <= 0) + goto end; + + /* With asymetric crypto algorithms we should always have a CEK */ + if (!items[JWE_ELT_CEK].length) + goto end; + + cek = &decoded_items[JWE_ELT_CEK]; + + *cek = alloc_trash_chunk(); + if (!*cek) + goto end; + + decrypted_cek = alloc_trash_chunk(); + if (!decrypted_cek) { + goto end; + } + + size = base64urldec(items[JWE_ELT_CEK].start, items[JWE_ELT_CEK].length, + (*cek)->area, (*cek)->size); + if (size < 0) { + goto end; + } + (*cek)->data = size; + + if (rsa && !decrypt_cek_rsa(*cek, decrypted_cek, cert, alg)) + goto end; + + if (decrypt_ciphertext(enc, items, decoded_items, decrypted_cek, &out)) + goto end; + + smp->data.u.str.data = b_data(out); + smp->data.u.str.area = b_orig(out); + smp->data.type = SMP_T_BIN; + smp_dup(smp); + + retval = 1; + +end: + free_trash_chunk(input); + free_trash_chunk(cert); + free_trash_chunk(decrypted_cek); + free_trash_chunk(out); + clear_decoded_items(decoded_items); + return retval; +} + +/* "jwt_decrypt_cert" converter check function. + * The first and only parameter should be a path to a pem certificate or a + * variable holding a path to a pem certificate. The certificate must already + * exist in the certificate store. + * This converter will be used for JWEs with an RSA type "alg" field in their + * JOSE header. + */ +static int sample_conv_jwt_decrypt_cert_check(struct arg *args, struct sample_conv *conv, + const char *file, int line, char **err) +{ + vars_check_arg(&args[0], NULL); + + if (args[0].type == ARGT_STR) { + struct ckch_store *store = NULL; + + if (HA_SPIN_TRYLOCK(CKCH_LOCK, &ckch_lock)) + return 0; + store = ckchs_lookup(args[0].data.str.area); + if (!store) { + memprintf(err, "unknown certificate %s", args[0].data.str.area); + HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock); + return 0; + } else if (!store->conf.jwt) { + memprintf(err, "unusable certificate %s (\"jwt\" option not set to \"on\")", args[0].data.str.area); + HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock); + return 0; + } + HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock); + } + + return 1; +} + /* "jwt_decrypt_secret" converter check function. * The first and only parameter should be a base64 encoded secret or a variable * holding a base64 encoded secret. This converter will be used mainly for JWEs @@ -663,6 +886,7 @@ static int sample_conv_jwt_decrypt_secret_check(struct arg *args, struct sample_ 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 }, + { "jwt_decrypt_cert", sample_conv_jwt_decrypt_cert, ARG1(1,STR), sample_conv_jwt_decrypt_cert_check, SMP_T_BIN, SMP_T_BIN }, { NULL, NULL, 0, 0, 0 }, }};