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.
This commit is contained in:
Remi Tricot-Le Breton 2025-10-02 15:32:41 +02:00 committed by William Lallemand
parent c3c0597a34
commit f5632fd481
5 changed files with 185 additions and 15 deletions

View 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 {

View 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 */

View 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}

View File

@ -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)) {
if (eb) {
entry = ebmb_entry(eb, struct jwt_cert_tree_entry, node);
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);
pubkey = entry->pubkey;
if (pubkey)
EVP_PKEY_up_ref(pubkey);
if (eb) {
entry = ebmb_entry(eb, struct jwt_cert_tree_entry, node);
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. */

View 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 },