]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MINOR: jwt: Add new jwt_verify_cert converter
authorRemi Tricot-Le Breton <rlebreton@haproxy.com>
Thu, 2 Oct 2025 13:32:41 +0000 (15:32 +0200)
committerWilliam Lallemand <wlallemand@haproxy.com>
Mon, 13 Oct 2025 08:38:52 +0000 (10:38 +0200)
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.

include/haproxy/jwt-t.h
include/haproxy/jwt.h
reg-tests/jwt/jws_verify.vtc
src/jwt.c
src/sample.c

index d5fda19cd2be28a4866fe2c2bf4e19824608db1b..fca752ef0c7633f6f94e6a7630a94748359e0424 100644 (file)
@@ -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 {
index 41b5bcaa9664eab6a3eb759b917b3ca2cf8399ed..4d0c92b0261b8c1fe28430cf8b8633474a69d6ab 100644 (file)
@@ -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 */
 
index 3e7b349e677aada9930c9be8e5c6f9bcd3b4cec6..9c1aad6a8bde91e870af0a57b0d32000b715cbe6 100644 (file)
@@ -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}
 
index f3e3385e9f54ec752fab1979611033a37be247bf..d804764054698c03602bf15f781a45b5920f8ad7 100644 (file)
--- 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 <token> that was signed via algorithm <alg> using the <key>
  * (either an HMAC secret or the path to a public certificate) has a valid
  * signature.
+ * <key> is either a HMAC secret or a public key path if <is_509_path> is 0,
+ * otherwise <key> 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. */
 
index 650652c695eaf32132e24282ad5411bf2a8d4d8b..e3711c1a7c022fd4303f0910f2d12e7820a391da 100644 (file)
@@ -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 },