]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MAJOR: jwt: Allow certificate instead of public key in jwt_verify converter
authorRemi Tricot-Le Breton <rlebreton@haproxy.com>
Mon, 30 Jun 2025 14:56:26 +0000 (16:56 +0200)
committerWilliam Lallemand <wlallemand@haproxy.com>
Mon, 30 Jun 2025 15:59:55 +0000 (17:59 +0200)
The 'jwt_verify' converter could only be passed public keys as second
parameter instead of full-on public certificates. This patch allows
proper certificates to be used.
Those certificates can be loaded in ckch_stores like any other
certificate which means that all the certificate-related operations that
can be made via the CLI can now benefit JWT validation as well.

We now have two ways JWT validation can work, the legacy one which only
relies on public keys which could not be stored in ckch_stores without
some in depth changes in the way the ckch_stores are built. In this
legacy way, the public keys are fully stored in a cache dedicated to JWT
only which does not have any CLI commands and any way to update them
during runtime. It also requires that all the public keys used are
passed at least once explicitely to the 'jwt_verify' converter so that
they can be loaded during init.
The new way uses actual certificates, either already stored in the
ckch_store tree (if predefined in a crt-store or already used previously
in the configuration) or loaded in the ckch_store tree during init if
they are explicitely used in the configuration like so:
    var(txn.bearer),jwt_verify(txn.jwt_alg,"cert.pem")

When using a variable (or any other way that can only be resolved during
runtime) in place of the converter's <key> parameter, the first time we
encounter a new value (for which we don't have any entry in the jwt
tree) we will lock the ckch_store tree and try to perform a lookup in
it. If the lookup fails, an entry will still be inserted into the jwt
tree so that any following call with this value avoids performing the
ckch_store tree lookup.

include/haproxy/jwt-t.h
include/haproxy/jwt.h
include/haproxy/ssl_ckch-t.h
include/haproxy/thread-t.h
src/jwt.c
src/sample.c
src/ssl_ckch.c
src/thread.c

index 4f611b38e9f6385336250bb8d5ed8fb39c29b34e..1372fddac3f7ad00172c30167d5b93106e41aa0b 100644 (file)
@@ -64,8 +64,17 @@ enum jwt_elt {
        JWT_ELT_MAX
 };
 
+enum jwt_entry_type {
+       JWT_ENTRY_DFLT,
+       JWT_ENTRY_STORE,
+       JWT_ENTRY_PKEY,
+       JWT_ENTRY_INVALID, /* already tried looking into ckch_store tree (unsuccessful) */
+};
+
 struct jwt_cert_tree_entry {
        EVP_PKEY *pubkey;
+       struct ckch_store *ckch_store;
+       int type;       /* jwt_entry_type */
        struct ebmb_node node;
        char path[VAR_ARRAY];
 };
@@ -78,7 +87,8 @@ enum jwt_vrfy_status {
        JWT_VRFY_UNMANAGED_ALG = -2,
        JWT_VRFY_INVALID_TOKEN = -3,
        JWT_VRFY_OUT_OF_MEMORY = -4,
-       JWT_VRFY_UNKNOWN_CERT  = -5
+       JWT_VRFY_UNKNOWN_CERT  = -5,
+       JWT_VRFY_INTERNAL_ERR  = -6
 };
 
 #endif /* USE_OPENSSL */
index a343ffaf75b8ee9494e9d8642ed2f6ec5af96da3..9e2df709a6ee9aa36febe551436c5b3d73871fce 100644 (file)
 #ifdef USE_OPENSSL
 enum jwt_alg jwt_parse_alg(const char *alg_str, unsigned int alg_len);
 int jwt_tokenize(const struct buffer *jwt, struct jwt_item *items, unsigned int *item_num);
-int jwt_tree_load_cert(char *path, int pathlen, char **err);
+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);
+
+void jwt_replace_ckch_store(struct ckch_store *old_ckchs, struct ckch_store *new_ckchs);
+
 #endif /* USE_OPENSSL */
 
 #endif /* _HAPROXY_JWT_H */
index 00a1d729bf6c6f7cc8d59c04d633842434ae9a7e..f3076e9a768dfc76c88c22d89a6b1d0e562f9687 100644 (file)
@@ -73,6 +73,8 @@ struct ckch_conf {
        } acme;
 };
 
+struct jwt_cert_tree_entry;
+
 /*
  * this is used to store 1 to SSL_SOCK_NUM_KEYTYPES cert_key_and_chain and
  * metadata.
@@ -88,6 +90,7 @@ struct ckch_store {
        struct list crtlist_entry; /* list of entries which use this store */
        struct ckch_conf conf;
        struct task *acme_task;
+       struct jwt_cert_tree_entry *jwt_entry;
        struct ebmb_node node;
        char path[VAR_ARRAY];
 };
index 2c99a4cb98194e3c94c2545a9042afd9b9a7c515..c9683542f2f5e769d514f0fe3ea9b518b2156efa 100644 (file)
@@ -217,6 +217,7 @@ enum lock_label {
        QC_CID_LOCK,
        CACHE_LOCK,
        GUID_LOCK,
+       JWT_LOCK,
        OTHER_LOCK,
        /* WT: make sure never to use these ones outside of development,
         * we need them for lock profiling!
index 17d32cd16e615579da7f64221c62ded193928790..a545c26298ca96792726f883c1ab777e41307d72 100644 (file)
--- a/src/jwt.c
+++ b/src/jwt.c
@@ -10,6 +10,8 @@
  * 2 of the License, or (at your option) any later version.
  */
 
+#include <sys/stat.h>
+
 #include <import/ebmbtree.h>
 #include <import/ebsttree.h>
 
 #include <haproxy/base64.h>
 #include <haproxy/jwt.h>
 #include <haproxy/buf.h>
+#include <haproxy/ssl_ckch.h>
+#include <haproxy/ssl_sock.h>
 
 
 #ifdef USE_OPENSSL
+
 /* Tree into which the public certificates used to validate JWTs will be stored. */
-static struct eb_root jwt_cert_tree = EB_ROOT_UNIQUE;
+struct eb_root jwt_cert_tree = EB_ROOT_UNIQUE;
+__decl_rwlock(jwt_tree_lock);
+
 
 /*
  * The possible algorithm strings that can be found in a JWS's JOSE header are
@@ -125,14 +132,24 @@ int jwt_tokenize(const struct buffer *jwt, struct jwt_item *items, unsigned int
 
 /*
  * Parse a public certificate and insert it into the jwt_cert_tree.
+ * This function can only be called during configuration parsing so we do not
+ * need to lock the jwt certificate tree.
  * Returns 0 in case of success.
  */
-int jwt_tree_load_cert(char *path, int pathlen, char **err)
+int jwt_tree_load_cert(char *path, int pathlen, const char *file, int line, char **err)
 {
        int retval = -1;
        struct jwt_cert_tree_entry *entry = NULL;
        EVP_PKEY *pubkey = NULL;
        BIO *bio = NULL;
+       struct stat buf;
+       struct ebmb_node *eb = NULL;
+       struct ckch_store *store = NULL;
+
+       eb = ebst_lookup(&jwt_cert_tree, path);
+
+       if (eb)
+               return 0; /* Entry already in the tree, nothing to do. */
 
        entry = calloc(1, sizeof(*entry) + pathlen + 1);
        if (!entry) {
@@ -140,31 +157,90 @@ int jwt_tree_load_cert(char *path, int pathlen, char **err)
                return -1;
        }
        memcpy(entry->path, path, pathlen + 1);
+       entry->type = JWT_ENTRY_DFLT;
 
        if (ebst_insert(&jwt_cert_tree, &entry->node) != &entry->node) {
+               /* Should never happen since we checked if the entry already
+                * existed previously.
+                */
                free(entry);
-               return 0; /* Entry already in the tree */
+               return 0;
        }
 
-       bio = BIO_new(BIO_s_file());
-       if (!bio) {
-               memprintf(err, "%sunable to allocate memory (BIO).\n", err && *err ? *err : "");
-               goto end;
+       if (stat(path, &buf) == 0) {
+               bio = BIO_new(BIO_s_file());
+               if (!bio) {
+                       memprintf(err, "%sunable to allocate memory (BIO).\n", err && *err ? *err : "");
+                       goto end;
+               }
+
+               if (BIO_read_filename(bio, path) == 1) {
+                       pubkey = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL);
+
+                       /* The file might exist but not contain a public key if
+                        * we were given an actual certificate path (or a
+                        * named crt-store).
+                        */
+                       if (pubkey) {
+                               entry->type = JWT_ENTRY_PKEY;
+                               entry->pubkey = pubkey;
+                               retval = 0;
+                               goto end;
+                       }
+               }
        }
 
-       if (BIO_read_filename(bio, path) == 1) {
+       /* 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;
 
-               pubkey = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL);
+               err_code = ckch_store_load_files(&conf, store,  0, file, line, err);
+               if (err_code & ERR_FATAL) {
+                       ckch_store_free(store);
 
-               if (!pubkey) {
-                       memprintf(err, "%sfile not found (%s)\n", err && *err ? *err : "", path);
+                       /* 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;
                }
 
-               entry->pubkey = pubkey;
-               retval = 0;
+               if (ebst_insert(&ckchs_tree, &store->node) != &store->node) {
+                       ckch_store_free(store);
+                       HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock);
+                       goto end;
+               }
        }
 
+       retval = 0;
+
+       BUG_ON(store->jwt_entry != NULL);
+       entry->type = JWT_ENTRY_STORE;
+       entry->ckch_store = store;
+       entry->pubkey = X509_get_pubkey(store->data->cert);
+       store->jwt_entry = entry;
+       HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock);
+
 end:
        if (retval) {
                /* Some error happened during pubkey parsing, remove the already
@@ -174,9 +250,67 @@ end:
                free(entry);
        }
        BIO_free(bio);
+
        return retval;
 }
 
+
+/* Try to look for an already existing ckch_store in the store tree with the
+ * path found in the jwt_entry. Keep a reference to its pubkey if it exists.
+ * Return 0 in case of success.
+ */
+static int jwt_tree_tryload_store(struct jwt_cert_tree_entry *jwt_entry)
+{
+       struct ckch_store *store = NULL;
+       int retval = 1;
+
+       if (!jwt_entry)
+               return 1;
+
+       /* We might have been given a 'crt-store' name */
+       if (HA_SPIN_TRYLOCK(CKCH_LOCK, &ckch_lock))
+               return 1;
+
+       store = ckchs_lookup(jwt_entry->path);
+       if (!store || store->jwt_entry)
+               goto end;
+
+       store->jwt_entry = jwt_entry;
+
+       BUG_ON(jwt_entry->pubkey != NULL);
+       jwt_entry->pubkey = X509_get_pubkey(store->data->cert);
+
+       retval = 0;
+
+end:
+       HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock);
+       return retval;
+
+}
+
+/* Update the ckch_store and public key reference of a jwt_entry. This is only
+ * useful whne updating a certificate from the CLI if it was being used for JWT
+ * validation.
+ */
+void jwt_replace_ckch_store(struct ckch_store *old_ckchs, struct ckch_store *new_ckchs)
+{
+       struct jwt_cert_tree_entry *entry = old_ckchs->jwt_entry;
+
+       HA_RWLOCK_WRLOCK(JWT_LOCK, &jwt_tree_lock);
+
+       if (entry == NULL)
+               goto end;
+
+       old_ckchs->jwt_entry->ckch_store = new_ckchs;
+       new_ckchs->jwt_entry = old_ckchs->jwt_entry;
+
+       EVP_PKEY_free(entry->pubkey);
+       entry->pubkey = X509_get_pubkey(new_ckchs->data->cert);
+
+end:
+       HA_RWLOCK_WRUNLOCK(JWT_LOCK, &jwt_tree_lock);
+}
+
 /*
  * Calculate the HMAC signature of a specific JWT and check that it matches the
  * one included in the token.
@@ -277,10 +411,12 @@ jwt_jwsverify_rsa_ecdsa(const struct jwt_ctx *ctx, struct buffer *decoded_signat
        EVP_MD_CTX *evp_md_ctx;
        EVP_PKEY_CTX *pkey_ctx = NULL;
        enum jwt_vrfy_status retval = JWT_VRFY_KO;
-       struct ebmb_node *eb;
+       struct ebmb_node *eb = NULL;
        struct jwt_cert_tree_entry *entry = NULL;
        int is_ecdsa = 0;
        int padding = RSA_PKCS1_PADDING;
+       EVP_PKEY *pubkey = NULL;
+       int lock_iswrlock = 0;
 
        switch(ctx->alg) {
        case JWS_ALG_RS256:
@@ -325,19 +461,71 @@ jwt_jwsverify_rsa_ecdsa(const struct jwt_ctx *ctx, struct buffer *decoded_signat
        if (!evp_md_ctx)
                return JWT_VRFY_OUT_OF_MEMORY;
 
+       HA_RWLOCK_RDLOCK(JWT_LOCK, &jwt_tree_lock);
+
        eb = ebst_lookup(&jwt_cert_tree, ctx->key);
 
        if (!eb) {
+               HA_RWLOCK_RDUNLOCK(JWT_LOCK, &jwt_tree_lock);
+
+               /* Create new entry and insert it in the jwt cert tree if we
+                * could find a corresponding ckch_store.
+                */
+               entry = calloc(1, sizeof(*entry) + ctx->key_length + 1);
+               if (!entry) {
+                       retval = JWT_VRFY_OUT_OF_MEMORY;
+                       goto end;
+               }
+               memcpy(entry->path, ctx->key, ctx->key_length);
+
+               /* The ckch_lock will be taken in jwt_tree_tryload_store so we
+                * can't hold the lock on the jwt_cert_tree here because the lock
+                * order is different when updating a certificate from the CLI,
+                * where the ckch_lock is taken first and then the JWT one is
+                * taken in jwt_replace_ckch_store.
+                * If no corresponding ckch_store was found, we still try to
+                * insert the entry in the tree so that next calls to jwt_verify
+                * with the same 'key' path do not perform the lookup in the
+                * ckch_store anymore. */
+               entry->type = (jwt_tree_tryload_store(entry) == 0) ? JWT_ENTRY_STORE : JWT_ENTRY_INVALID;
+
+               HA_RWLOCK_WRLOCK(JWT_LOCK, &jwt_tree_lock);
+               if (ebst_insert(&jwt_cert_tree, &entry->node) != &entry->node) {
+                       /* This rather unlikely case can only happen if the tree was
+                        * modified between the previous read unlock and here.
+                        */
+                       retval = JWT_VRFY_INTERNAL_ERR;
+                       free(entry);
+                       entry = NULL;
+                       HA_RWLOCK_WRUNLOCK(JWT_LOCK, &jwt_tree_lock);
+                       goto end;
+               }
+               lock_iswrlock = 1;
+       } else {
+               entry = ebmb_entry(eb, struct jwt_cert_tree_entry, node);
+       }
+
+       /* We tried looking for a ckch_store but could not find it */
+       switch (entry->type) {
+       case JWT_ENTRY_PKEY:
+       case JWT_ENTRY_STORE:
+               pubkey = entry->pubkey;
+               if (pubkey)
+                       EVP_PKEY_up_ref(pubkey);
+               break;
+       case JWT_ENTRY_DFLT:
+       case JWT_ENTRY_INVALID:
                retval = JWT_VRFY_UNKNOWN_CERT;
-               goto end;
+               break;
        }
 
-       entry = ebmb_entry(eb, struct jwt_cert_tree_entry, node);
+       if (lock_iswrlock)
+               HA_RWLOCK_WRUNLOCK(JWT_LOCK, &jwt_tree_lock);
+       else
+               HA_RWLOCK_RDUNLOCK(JWT_LOCK, &jwt_tree_lock);
 
-       if (!entry->pubkey) {
-               retval = JWT_VRFY_UNKNOWN_CERT;
+       if (!pubkey)
                goto end;
-       }
 
        /*
         * ECXXX signatures are a direct concatenation of the (R, S) pair and
@@ -352,7 +540,7 @@ jwt_jwsverify_rsa_ecdsa(const struct jwt_ctx *ctx, struct buffer *decoded_signat
                }
        }
 
-       if (EVP_DigestVerifyInit(evp_md_ctx, &pkey_ctx, evp, NULL, entry->pubkey) == 1) {
+       if (EVP_DigestVerifyInit(evp_md_ctx, &pkey_ctx, evp, NULL, pubkey) == 1) {
                if (is_ecdsa || EVP_PKEY_CTX_set_rsa_padding(pkey_ctx, padding) > 0) {
                        if (EVP_DigestVerifyUpdate(evp_md_ctx, (const unsigned char*)ctx->jose.start,
                                                   ctx->jose.length + ctx->claims.length + 1) == 1 &&
@@ -371,6 +559,7 @@ end:
                 */
                ERR_clear_error();
        }
+       EVP_PKEY_free(pubkey);
        return retval;
 }
 
@@ -470,6 +659,8 @@ static void jwt_deinit(void)
        struct ebmb_node *node = NULL;
        struct jwt_cert_tree_entry *entry = NULL;
 
+       HA_RWLOCK_WRLOCK(JWT_LOCK, &jwt_tree_lock);
+
        node = ebmb_first(&jwt_cert_tree);
        while (node) {
                entry = ebmb_entry(node, struct jwt_cert_tree_entry, node);
@@ -478,6 +669,8 @@ static void jwt_deinit(void)
                ha_free(&entry);
                node = ebmb_first(&jwt_cert_tree);
        }
+
+       HA_RWLOCK_WRUNLOCK(JWT_LOCK, &jwt_tree_lock);
 }
 REGISTER_POST_DEINIT(jwt_deinit);
 
index b91e97ce5e39f6d1e92a561e0783164048ca9240..77c2977cfa37cdcc5618a315d4278341d85ab730 100644 (file)
@@ -4457,6 +4457,7 @@ static int sample_conv_jwt_verify_check(struct arg *args, struct sample_conv *co
                                        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);
@@ -4476,14 +4477,22 @@ static int sample_conv_jwt_verify_check(struct arg *args, struct sample_conv *co
                        JWS_ALG_HS384:
                        JWS_ALG_HS512:
                        /* don't try to load a file with HMAC algorithms */
+                               retval = 1;
                                break;
                        default:
-                               jwt_tree_load_cert(args[1].data.str.area, args[1].data.str.data, err);
+                               retval = (jwt_tree_load_cert(args[1].data.str.area, args[1].data.str.data,
+                                                            file, line, err) == 0);
                                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 1;
+       return retval;
 }
 
 /* Check that a JWT's signature is correct */
index 9157421b4b9759f36bad5afb49487851f1542d79..75dc02caa1fd2f6f0eda0246bc7d79deeaee3806 100644 (file)
@@ -41,6 +41,7 @@
 #include <haproxy/ssl_utils.h>
 #include <haproxy/stconn.h>
 #include <haproxy/tools.h>
+#include <haproxy/jwt.h>
 
 /* Uncommitted CKCH transaction */
 
@@ -2729,6 +2730,8 @@ void ckch_store_replace(struct ckch_store *old_ckchs, struct ckch_store *new_ckc
                __ckch_inst_free_locked(ckchi);
        }
 
+       jwt_replace_ckch_store(old_ckchs, new_ckchs);
+
        ckch_store_free(old_ckchs);
        ebst_insert(&ckchs_tree, &new_ckchs->node);
 }
index 8c0c5c2940f1b15070051e405df46852ed4a38ff..266c1562bdfff4827e273d0c97ae2a5f51228fa1 100644 (file)
@@ -437,6 +437,7 @@ const char *lock_label(enum lock_label label)
        case QC_CID_LOCK:          return "QC_CID";
        case CACHE_LOCK:           return "CACHE";
        case GUID_LOCK:            return "GUID";
+       case JWT_LOCK:             return "JWT";
        case OTHER_LOCK:           return "OTHER";
        case DEBUG1_LOCK:          return "DEBUG1";
        case DEBUG2_LOCK:          return "DEBUG2";