MINOR: jwt: Add new jwt_decrypt converter

This converter takes a private key in the JWK format (RFC7517) that can
be provided as a string of via a variable.
The only keys managed for now are of type 'RSA' or 'oct'.
This commit is contained in:
Remi Tricot-Le Breton 2026-02-03 16:58:33 +01:00 committed by William Lallemand
parent 0023596164
commit c98002337c
2 changed files with 354 additions and 2 deletions

View file

@ -20595,6 +20595,7 @@ ip.ver binary integer
ipmask(mask4[,mask6]) address address
json([input-code]) string string
json_query(json_path[,output_type]) string _outtype_
jwk_decrypt(<jwk>) string binary
jwt_decrypt_cert(<cert>) string binary
jwt_decrypt_secret(<secret>) string binary
jwt_header_query([json_path[,output_type]]) string string
@ -21424,6 +21425,44 @@ json_query(<json_path>[,<output_type>])
# get the value of the key 'iss' from a JWT Bearer token
http-request set-var(txn.token_payload) req.hdr(Authorization),word(2,.),ub64dec,json_query('$.iss')
jwk_decrypt(<jwk>)
Performs a signature validation of a JSON Web Token following the JSON Web
Encryption format (see RFC 7516) given in input and return its content
decrypted thanks to the provided JSON Web Key (RFC7517).
The <jwk> parameter must be a valid JWK of type 'oct' or 'RSA' ('kty' field
of the JSON key) that can be provided either as a string or via a variable.
The only tokens managed yet are the ones using the Compact Serialization
format (five dot-separated base64-url encoded strings).
This converter can be used to decode token that have a symmetric-type
algorithm ("alg" field of the JOSE header) among the following: A128KW,
A192KW, A256KW, A128GCMKW, A192GCMKW, A256GCMKW, dir. In this case, we expect
the provided JWK to be of the 'oct' type. Please note that the A128KW and
A192KW algorithms are not available on AWS-LC and decryption will not work.
This converter also manages tokens that have an algorithm ("alg" field of
the JOSE header) among the following: RSA1_5, RSA-OAEP or RSA-OAEP-256. In
such a case an 'RSA' type JWK representing a private key must be provided.
The JWE token must be provided base64url-encoded and the output will be
provided "raw". If an error happens during token parsing, signature
verification or content decryption, an empty string will be returned.
Because of the way quotes, commas and double quotes are treated in the
configuration, the contents of the JWK must be properly escaped for this
converter to work properly (see section 2.2 for more information).
Example:
# Get a JWT from the authorization header, put its decrypted content in an
# HTTP header
http-request set-var(txn.bearer) http_auth_bearer
http-request set-header X-Decrypted %[var(txn.bearer),jwt_decrypt_secret(\'{\"kty\":\"oct\",\"k\":\"wAsgsg\"}\')
# or via a variable
http-request set-var(txn.bearer) http_auth_bearer
http-request set-var(txn.jwk) str(\'{\"kty\":\"oct\",\"k\":\"Q-NFLlghQ\"}\')
http-request set-header X-Decrypted %[var(txn.bearer),jwt_decrypt_secret(txn.jwk)
jwt_decrypt_cert(<cert>)
Performs a signature validation of a JSON Web Token following the JSON Web
Encryption format (see RFC 7516) given in input and return its content
@ -31730,8 +31769,8 @@ ocsp-update [ off | on ]
jwt [ off | on ]
Allow for this certificate to be used for JWT validation or decryption via
the "jwt_verify_cert" or "jwt_decrypt_cert" converters when set to 'on'. Its
value defaults to 'off'.
the "jwt_verify_cert", "jwt_decrypt_cert" or "jwt_decrypt" converters when
set to 'on'. Its value defaults to 'off'.
When set to 'on' for a given certificate, the CLI command "del ssl cert" will
not work. In order to be deleted, a certificate must not be used, either for

313
src/jwe.c
View file

@ -1118,10 +1118,323 @@ end:
}
typedef enum {
JWK_KTY_OCT,
JWK_KTY_RSA,
// JWK_KTY_EC
} jwk_type;
struct jwk {
jwk_type type;
struct buffer *kid;
union {
EVP_PKEY *pkey;
struct buffer *secret;
};
};
static void clear_jwk(struct jwk *jwk)
{
if (!jwk)
return;
free_trash_chunk(jwk->kid);
jwk->kid = NULL;
switch (jwk->type) {
case JWK_KTY_OCT:
free_trash_chunk(jwk->secret);
jwk->secret = NULL;
break;
case JWK_KTY_RSA:
EVP_PKEY_free(jwk->pkey);
jwk->pkey = NULL;
break;
default:
break;
}
}
/*
* Convert a JWK in buffer <jwk_buf> into either an RSA private key stored in an
* EVP_PKEY or a secret (for symmetric algorithms).
* Returns 0 in case of success, 1 otherwise.
*/
static int process_jwk(struct buffer *jwk_buf, struct jwk *jwk)
{
struct buffer *kty = NULL;
int retval = 1;
kty = get_trash_chunk();
if (get_jwk_field(jwk_buf, "$.kty", kty))
goto end;
/* Look for optional "kid" field */
jwk->kid = alloc_trash_chunk();
if (!jwk->kid)
goto end;
get_jwk_field(jwk_buf, "$.kid", jwk->kid);
if (chunk_strcmp(kty, "oct") == 0) {
struct buffer *tmpbuf = get_trash_chunk();
int size = 0;
jwk->type = JWK_KTY_OCT;
jwk->secret = alloc_trash_chunk();
if (!jwk->secret)
goto end;
if (get_jwk_field(jwk_buf, "$.k", tmpbuf))
goto end;
size = base64urldec(b_orig(tmpbuf), b_data(tmpbuf),
b_orig(jwk->secret), b_size(jwk->secret));
if (size < 0) {
goto end;
}
jwk->secret->data = size;
} else if (chunk_strcmp(kty, "RSA") == 0) {
jwk->type = JWK_KTY_RSA;
if (build_RSA_PKEY_from_buf(jwk_buf, &jwk->pkey))
goto end;
} else
goto end;
retval = 0;
end:
if (retval)
clear_jwk(jwk);
return retval;
}
static int sample_conv_jwt_decrypt_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) {
EVP_PKEY *pkey = NULL;
struct buffer *trash = get_trash_chunk();
if (get_jwk_field(&args[0].data.str, "$.kty", trash) == 0) {
if (chunk_strcmp(trash, "oct") == 0) {
struct buffer *key = get_trash_chunk();
if (get_jwk_field(&args[0].data.str, "$.k", key)) {
memprintf(err, "Missing 'k' field in JWK");
return 0;
}
} else if (chunk_strcmp(trash, "RSA") == 0) {
if (build_RSA_PKEY_from_buf(&args[0].data.str, &pkey)) {
memprintf(err, "Failed to parse JWK");
return 0;
}
EVP_PKEY_free(pkey);
} else {
memprintf(err, "Unmanaged key type (expected 'oct' or 'RSA'");
return 0;
}
} else {
memprintf(err, "Missing key type (expected 'oct' or 'RSA')");
return 0;
}
}
return 1;
}
/*
* Decrypt the contents of a JWE token thanks to the user-provided JWK that can
* either contain an RSA private key or a secret.
* Returns the decrypted contents, or nothing if any error happened.
*/
static int sample_conv_jwt_decrypt(const struct arg *args, struct sample *smp, void *private)
{
struct buffer *input = NULL;
unsigned int item_num = JWE_ELT_MAX;
struct sample jwk_smp;
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 size = 0;
int rsa = 0;
int dir = 0;
int gcm = 0;
int oct = 0;
int retval = 0;
struct buffer **cek = NULL;
struct buffer *decrypted_cek = NULL;
struct buffer *out = NULL;
struct jose_fields fields = {};
struct buffer *alg_tag = NULL;
struct buffer *alg_iv = NULL;
struct buffer *jwk_buf = NULL;
struct jwk jwk = {};
smp_set_owner(&jwk_smp, smp->px, smp->sess, smp->strm, smp->opt);
if (!sample_conv_var2smp_str(&args[0], &jwk_smp))
goto end;
/* Copy JWK parameter */
jwk_buf = alloc_trash_chunk();
if (!jwk_buf)
goto end;
if (!chunk_cpy(jwk_buf, &jwk_smp.data.u.str))
goto end;
/* Copy JWE input token */
input = alloc_trash_chunk();
if (!input)
goto end;
if (!chunk_cpy(input, &smp->data.u.str))
goto end;
if (jwt_tokenize(input, items, &item_num) || item_num != JWE_ELT_MAX)
goto end;
alg_tag = alloc_trash_chunk();
if (!alg_tag)
goto end;
alg_iv = alloc_trash_chunk();
if (!alg_iv)
goto end;
fields.tag = alg_tag;
fields.iv = alg_iv;
/* 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;
case JWE_ALG_A128KW:
case JWE_ALG_A192KW:
case JWE_ALG_A256KW:
gcm = 0;
oct = 1;
break;
case JWE_ALG_A128GCMKW:
case JWE_ALG_A192GCMKW:
case JWE_ALG_A256GCMKW:
gcm = 1;
oct = 1;
break;
case JWE_ALG_DIR:
dir = 1;
oct = 1;
break;
default:
/* Not managed yet */
goto end;
}
/* Parse JWK argument. */
if (process_jwk(jwk_buf, &jwk))
goto end;
/* Check that the provided JWK is of the proper type */
if ((oct && jwk.type != JWK_KTY_OCT) ||
(rsa && jwk.type != JWK_KTY_RSA))
goto end;
if (dir) {
/* The secret given as parameter should be used directly to
* decode the encrypted content. */
decrypted_cek = alloc_trash_chunk();
if (!decrypted_cek)
goto end;
chunk_memcpy(decrypted_cek, b_orig(jwk.secret), b_data(jwk.secret));
} else {
/* With algorithms other than "dir" 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) {
if (do_decrypt_cek_rsa(*cek, decrypted_cek, jwk.pkey, alg))
goto end;
} else {
if (gcm) {
if (!decrypt_cek_aesgcmkw(*cek, alg_tag, alg_iv, decrypted_cek, jwk.secret, alg))
goto end;
} else {
if (!decrypt_cek_aeskw(*cek, decrypted_cek, jwk.secret, 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:
clear_jwk(&jwk);
free_trash_chunk(jwk_buf);
free_trash_chunk(input);
free_trash_chunk(decrypted_cek);
free_trash_chunk(out);
free_trash_chunk(alg_tag);
free_trash_chunk(alg_iv);
clear_decoded_items(decoded_items);
return retval;
}
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 },
{ "jwt_decrypt", sample_conv_jwt_decrypt, ARG1(1,STR), sample_conv_jwt_decrypt_check, SMP_T_BIN, SMP_T_BIN },
{ NULL, NULL, 0, 0, 0 },
}};