From: Remi Tricot-Le Breton Date: Thu, 2 Oct 2025 13:32:41 +0000 (+0200) Subject: MINOR: jwt: Add new jwt_verify_cert converter X-Git-Tag: v3.3-dev10~29 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=f5632fd481eeb104203451fbe14fc0d7a9884e1e;p=thirdparty%2Fhaproxy.git MINOR: jwt: Add new jwt_verify_cert converter This converter will be in charge of performing the same operation as the 'jwt_verify' one except that it takes a full-on pem certificate path instead of a public key path as parameter. The certificate path can be either provided directly as a string or via a variable. This allows to use certificates that are not known during init to perform token validation. --- diff --git a/include/haproxy/jwt-t.h b/include/haproxy/jwt-t.h index d5fda19cd..fca752ef0 100644 --- a/include/haproxy/jwt-t.h +++ b/include/haproxy/jwt-t.h @@ -55,6 +55,7 @@ struct jwt_ctx { struct jwt_item signature; char *key; unsigned int key_length; + int is_x509; /* 1 if 'key' field is a certificate, 0 otherwise */ }; enum jwt_elt { diff --git a/include/haproxy/jwt.h b/include/haproxy/jwt.h index 41b5bcaa9..4d0c92b02 100644 --- a/include/haproxy/jwt.h +++ b/include/haproxy/jwt.h @@ -31,7 +31,7 @@ int jwt_tokenize(const struct buffer *jwt, struct jwt_item *items, unsigned int int jwt_tree_load_cert(char *path, int pathlen, const char *file, int line, char **err); enum jwt_vrfy_status jwt_verify(const struct buffer *token, const struct buffer *alg, - const struct buffer *key); + const struct buffer *key, int is_x509); #endif /* USE_OPENSSL */ diff --git a/reg-tests/jwt/jws_verify.vtc b/reg-tests/jwt/jws_verify.vtc index 3e7b349e6..9c1aad6a8 100644 --- a/reg-tests/jwt/jws_verify.vtc +++ b/reg-tests/jwt/jws_verify.vtc @@ -80,17 +80,17 @@ haproxy h1 -conf { http-response set-header x-jwt-verify-RS512 %[var(txn.bearer),jwt_verify(txn.jwt_alg,"${testdir}/rsa-public.pem")] if { var(txn.jwt_alg) -m str "RS512" } # Pure certificate (not predefined in crt-store) - http-response set-header x-jwt-verify-RS256-cert %[var(txn.bearer),jwt_verify(txn.jwt_alg,"${testdir}/cert.rsa.pem")] if { var(txn.jwt_alg) -m str "RS256" } + http-response set-header x-jwt-verify-RS256-cert %[var(txn.bearer),jwt_verify_cert(txn.jwt_alg,"${testdir}/cert.rsa.pem")] if { var(txn.jwt_alg) -m str "RS256" } # Named crt-store - http-response set-header x-jwt-verify-RS256-cert-named %[var(txn.bearer),jwt_verify(txn.jwt_alg,"@named_store${testdir}/cert.rsa.pem")] if { var(txn.jwt_alg) -m str "RS256" } + http-response set-header x-jwt-verify-RS256-cert-named %[var(txn.bearer),jwt_verify_cert(txn.jwt_alg,"@named_store${testdir}/cert.rsa.pem")] if { var(txn.jwt_alg) -m str "RS256" } # Variables # This first case only works because the certificate # is already explicitly used in a previous jwt_verify call. http-response set-var(txn.cert) str("${testdir}/cert.rsa.pem") - http-response set-header x-jwt-verify-RS256-var1 %[var(txn.bearer),jwt_verify(txn.jwt_alg,txn.cert)] if { var(txn.jwt_alg) -m str "RS256" } + http-response set-header x-jwt-verify-RS256-var1 %[var(txn.bearer),jwt_verify_cert(txn.jwt_alg,txn.cert)] if { var(txn.jwt_alg) -m str "RS256" } http-response set-var(txn.cert) str("@named_store${testdir}/cert.rsa.pem") - http-response set-header x-jwt-verify-RS256-var2 %[var(txn.bearer),jwt_verify(txn.jwt_alg,txn.cert)] if { var(txn.jwt_alg) -m str "RS256" } + http-response set-header x-jwt-verify-RS256-var2 %[var(txn.bearer),jwt_verify_cert(txn.jwt_alg,txn.cert)] if { var(txn.jwt_alg) -m str "RS256" } server s1 ${s1_addr}:${s1_port} @@ -109,7 +109,7 @@ haproxy h1 -conf { # Variables and real certificate http-response set-var(txn.cert) str("${testdir}/cert.ecdsa.pem") - http-response set-header x-jwt-verify-ES256-var %[var(txn.bearer),jwt_verify(txn.jwt_alg,txn.cert)] if { var(txn.jwt_alg) -m str "ES256" } + http-response set-header x-jwt-verify-ES256-var %[var(txn.bearer),jwt_verify_cert(txn.jwt_alg,txn.cert)] if { var(txn.jwt_alg) -m str "ES256" } server s1 ${s1_addr}:${s1_port} diff --git a/src/jwt.c b/src/jwt.c index f3e3385e9..d80476405 100644 --- a/src/jwt.c +++ b/src/jwt.c @@ -141,6 +141,7 @@ int jwt_tree_load_cert(char *path, int pathlen, const char *file, int line, char BIO *bio = NULL; struct stat buf; struct ebmb_node *eb = NULL; + struct ckch_store *store = NULL; eb = ebst_lookup(&jwt_cert_tree, path); @@ -181,6 +182,52 @@ int jwt_tree_load_cert(char *path, int pathlen, const char *file, int line, char } } + /* Look for an actual certificate or crt-store with the given name. + * If the path corresponds to an actual certificate that was not loaded + * yet we will create the corresponding ckch_store. */ + if (HA_SPIN_TRYLOCK(CKCH_LOCK, &ckch_lock)) + goto end; + + store = ckchs_lookup(path); + if (!store) { + struct ckch_conf conf = {}; + int err_code = 0; + + /* Create a new store with the given path */ + store = ckch_store_new(path); + if (!store) { + HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock); + goto end; + } + + conf.crt = path; + + err_code = ckch_store_load_files(&conf, store, 0, file, line, err); + if (err_code & ERR_FATAL) { + ckch_store_free(store); + + /* If we are in this case we are in the conf + * parsing phase and this case might happen if + * we were provided an HMAC secret or a variable + * name. + */ + retval = 0; + ha_free(err); + HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock); + goto end; + } + + if (ebst_insert(&ckchs_tree, &store->node) != &store->node) { + ckch_store_free(store); + HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock); + goto end; + } + } + + retval = 0; + + HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock); + end: if (retval) { /* Some error happened during pubkey parsing, remove the already @@ -209,6 +256,10 @@ jwt_jwsverify_hmac(const struct jwt_ctx *ctx, const struct buffer *decoded_signa unsigned char *hmac_res = NULL; enum jwt_vrfy_status retval = JWT_VRFY_KO; + if (ctx->is_x509) { + return JWT_VRFY_UNMANAGED_ALG; + } + switch(ctx->alg) { case JWS_ALG_HS256: evp = EVP_sha256(); @@ -344,15 +395,29 @@ jwt_jwsverify_rsa_ecdsa(const struct jwt_ctx *ctx, struct buffer *decoded_signat if (!evp_md_ctx) return JWT_VRFY_OUT_OF_MEMORY; - /* Look for a public key in the JWT tree */ - eb = ebst_lookup(&jwt_cert_tree, ctx->key); + if (ctx->is_x509) { + struct ckch_store *store = NULL; + if (!HA_SPIN_TRYLOCK(CKCH_LOCK, &ckch_lock)) { + + store = ckchs_lookup(ctx->key); + if (store) { + pubkey = X509_get_pubkey(store->data->cert); + if (pubkey) + EVP_PKEY_up_ref(pubkey); + } + HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock); + } + } else { + /* Look for a public key in the JWT tree */ + eb = ebst_lookup(&jwt_cert_tree, ctx->key); - if (eb) { - entry = ebmb_entry(eb, struct jwt_cert_tree_entry, node); + if (eb) { + entry = ebmb_entry(eb, struct jwt_cert_tree_entry, node); - pubkey = entry->pubkey; - if (pubkey) - EVP_PKEY_up_ref(pubkey); + pubkey = entry->pubkey; + if (pubkey) + EVP_PKEY_up_ref(pubkey); + } } if (!pubkey) { @@ -400,10 +465,12 @@ end: * Check that the that was signed via algorithm using the * (either an HMAC secret or the path to a public certificate) has a valid * signature. + * is either a HMAC secret or a public key path if is 0, + * otherwise is an X509 certificate path. * Returns 1 in case of success. */ enum jwt_vrfy_status jwt_verify(const struct buffer *token, const struct buffer *alg, - const struct buffer *key) + const struct buffer *key, int is_x509_path) { struct jwt_item items[JWT_ELT_MAX] = { { 0 } }; unsigned int item_num = JWT_ELT_MAX; @@ -450,6 +517,7 @@ enum jwt_vrfy_status jwt_verify(const struct buffer *token, const struct buffer decoded_sig->data = ret; ctx.key = key->area; ctx.key_length = key->data; + ctx.is_x509 = is_x509_path; /* We have all three sections, signature calculation can begin. */ diff --git a/src/sample.c b/src/sample.c index 650652c69..e3711c1a7 100644 --- a/src/sample.c +++ b/src/sample.c @@ -4524,6 +4524,53 @@ static int sample_conv_jwt_verify_check(struct arg *args, struct sample_conv *co /* don't try to load a file with HMAC algorithms */ retval = 1; break; + default: + retval = (jwt_tree_load_cert(args[1].data.str.area, args[1].data.str.data, + file, line, err) == 0); + /* The second arg might be an HMAC secret but + * the 'alg' is stored in a var */ + if (!retval && args[0].type == ARGT_VAR) + retval = 1; + break; + } + } else if (args[1].type == ARGT_VAR) { + /* We will try to resolve the var during runtime because the + * processing might work if it actually points to an already + * existing ckch_store. + */ + retval = 1; + } + + return retval; +} + +static int sample_conv_jwt_verify_cert_check(struct arg *args, struct sample_conv *conv, + const char *file, int line, char **err) +{ + enum jwt_alg alg = JWT_ALG_DEFAULT; + int retval = 0; + + vars_check_arg(&args[0], NULL); + vars_check_arg(&args[1], NULL); + + if (args[0].type == ARGT_STR) { + alg = jwt_parse_alg(args[0].data.str.area, args[0].data.str.data); + + if (alg == JWT_ALG_DEFAULT) { + memprintf(err, "unknown JWT algorithm: %s", args[0].data.str.area); + return 0; + } + } + + if (args[1].type == ARGT_STR) { + switch (alg) { + case JWS_ALG_HS256: + case JWS_ALG_HS384: + case JWS_ALG_HS512: + /* We can't have a certificate as second parameter for + * HMAC-signed JWT tokens */ + memprintf(err, "HMAC-signed tokens can't be processed by this converter"); + break; default: retval = (jwt_tree_load_cert(args[1].data.str.area, args[1].data.str.data, file, line, err) == 0); @@ -4580,7 +4627,60 @@ static int sample_conv_jwt_verify(const struct arg *args, struct sample *smp, vo if (chunk_printf(key, "%.*s", (int)b_data(&key_smp.data.u.str), b_orig(&key_smp.data.u.str)) <= 0) goto end; - ret = jwt_verify(input, alg, key); + ret = jwt_verify(input, alg, key, 0); + + smp->data.type = SMP_T_SINT; + smp->data.u.sint = ret; + + retval = 1; + +end: + free_trash_chunk(input); + free_trash_chunk(alg); + free_trash_chunk(key); + return retval; +} + +static int sample_conv_jwt_verify_cert(const struct arg *args, struct sample *smp, void *private) +{ + struct sample alg_smp, key_smp; + enum jwt_vrfy_status ret; + struct buffer *input = NULL; + struct buffer *alg = NULL; + struct buffer *key = NULL; + int retval = 0; + + /* The two following calls to 'sample_conv_var2smp_str' will both make + * use of the preallocated trash buffer (via get_trash_chunk call in + * smp_dup) which would end up erasing the contents of the 'smp' input + * buffer. + */ + input = alloc_trash_chunk(); + if (!input) + return 0; + alg = alloc_trash_chunk(); + if (!alg) + goto end; + key = alloc_trash_chunk(); + if (!key) + goto end; + + if (!chunk_cpy(input, &smp->data.u.str)) + goto end; + + smp_set_owner(&alg_smp, smp->px, smp->sess, smp->strm, smp->opt); + if (!sample_conv_var2smp_str(&args[0], &alg_smp)) + goto end; + if (chunk_printf(alg, "%.*s", (int)b_data(&alg_smp.data.u.str), b_orig(&alg_smp.data.u.str)) <= 0) + goto end; + + smp_set_owner(&key_smp, smp->px, smp->sess, smp->strm, smp->opt); + if (!sample_conv_var2smp_str(&args[1], &key_smp)) + goto end; + if (chunk_printf(key, "%.*s", (int)b_data(&key_smp.data.u.str), b_orig(&key_smp.data.u.str)) <= 0) + goto end; + + ret = jwt_verify(input, alg, key, 1); smp->data.type = SMP_T_SINT; smp->data.u.sint = ret; @@ -5532,6 +5632,7 @@ static struct sample_conv_kw_list sample_conv_kws = {ILH, { { "jwt_header_query", sample_conv_jwt_header_query, ARG2(0,STR,STR), sample_conv_jwt_query_check, SMP_T_BIN, SMP_T_ANY }, { "jwt_payload_query", sample_conv_jwt_payload_query, ARG2(0,STR,STR), sample_conv_jwt_query_check, SMP_T_BIN, SMP_T_ANY }, { "jwt_verify", sample_conv_jwt_verify, ARG2(2,STR,STR), sample_conv_jwt_verify_check, SMP_T_BIN, SMP_T_SINT }, + { "jwt_verify_cert", sample_conv_jwt_verify_cert, ARG2(2,STR,STR), sample_conv_jwt_verify_cert_check, SMP_T_BIN, SMP_T_SINT }, #endif { "when", sample_conv_when, ARG3(1,STR,STR,STR), check_when_cond, SMP_T_ANY, SMP_T_ANY }, { NULL, NULL, 0, 0, 0 },