From 026652a7eb30a9b4dcd24be08628fdc9629126a3 Mon Sep 17 00:00:00 2001 From: Remi Tricot-Le Breton Date: Tue, 10 Mar 2026 14:43:43 +0100 Subject: [PATCH] MINOR: jwt: Parse ec-specific fields in jose header When the encoding is of the ECDH family, the optional "apu" and "apv" fields of the JOSE header must be parsed, as well as the mandatory "epk" field that contains an EC public key used to derive a key that allows either to decrypt the contents of the token (in case of ECDH-ES) or to decrypt the content encoding key (cek) when using ECDH-ES+AES Key Wrap. --- src/jwe.c | 151 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 102 insertions(+), 49 deletions(-) diff --git a/src/jwe.c b/src/jwe.c index 3cc4f2583..81f837983 100644 --- a/src/jwe.c +++ b/src/jwe.c @@ -38,7 +38,7 @@ typedef enum { JWE_ALG_A192KW, JWE_ALG_A256KW, JWE_ALG_DIR, - // JWE_ALG_ECDH_ES, + JWE_ALG_ECDH_ES, // JWE_ALG_ECDH_ES_A128KW, // JWE_ALG_ECDH_ES_A192KW, // JWE_ALG_ECDH_ES_A256KW, @@ -114,6 +114,10 @@ enum jwe_elt { struct jose_fields { struct buffer *tag; struct buffer *iv; + struct buffer *epk; + EVP_PKEY *pubkey; + struct buffer *apu; + struct buffer *apv; }; @@ -137,11 +141,14 @@ static inline int parse_alg_enc(struct buffer *buf, struct alg_enc *array) } /* - * Look for field in JSON and base64url decode its - * content in buffer . - * The field might not be found, it won't be raised as an error. + * Look for field in JSON and if it is found, + * allocate an buffer and base64url decode the field's content in it. + * If is set to 1, an error will be raised if the field is not + * found. + * Returns 0 in case of success, 1 in case of error (decoding or alloc error). */ -static inline int decode_jose_field(struct buffer *decoded_jose, const char *field_name, struct buffer *out) +static inline int decode_jose_field(struct buffer *decoded_jose, const char *field_name, + struct buffer **out, int mandatory) { struct buffer *trash = get_trash_chunk(); int size = 0; @@ -153,20 +160,31 @@ static inline int decode_jose_field(struct buffer *decoded_jose, const char *fie b_orig(trash), b_size(trash)); if (size != -1) { trash->data = size; - size = base64urldec(b_orig(trash), b_data(trash), - b_orig(out), b_size(out)); - if (size < 0) + + *out = alloc_trash_chunk(); + if (!*out) return 1; - out->data = size; - } + + size = base64urldec(b_orig(trash), b_data(trash), + b_orig(*out), b_size(*out)); + if (size < 0) { + free_trash_chunk(*out); + *out = NULL; + return 1; + } + (*out)->data = size; + } else if (mandatory) + return 1; return 0; } +static int build_EC_PKEY_from_buf(struct buffer *jwk, EVP_PKEY **pkey); /* - * Extract the "alg" and "enc" of the JOSE header as well as some algo-specific - * base64url encoded fields. + * Extract multiple base64url encoded fields from the JOSE header that are + * required for the subsequent decrypt operations. Depending on the used + * algorithms the required fields will change. */ static int parse_jose(struct buffer *decoded_jose, int *alg, int *enc, struct jose_fields *jose_fields) { @@ -174,6 +192,9 @@ static int parse_jose(struct buffer *decoded_jose, int *alg, int *enc, struct jo int retval = 0; int size = 0; + int gcm = 0; + int ec = 0; + /* Look for "alg" field */ trash = get_trash_chunk(); size = mjson_get_string(b_orig(decoded_jose), b_data(decoded_jose), "$.alg", @@ -196,13 +217,56 @@ static int parse_jose(struct buffer *decoded_jose, int *alg, int *enc, struct jo if (*enc == JWE_ENC_UNMANAGED) goto end; - /* Look for "tag" field (used by aes gcm encryption) */ - if (decode_jose_field(decoded_jose, "$.tag", jose_fields->tag)) - goto end; + switch (*alg) { + case JWE_ALG_ECDH_ES: + ec = 1; + break; + case JWE_ALG_A128GCMKW: + case JWE_ALG_A192GCMKW: + case JWE_ALG_A256GCMKW: + gcm = 1; + break; + default: break; + } - /* Look for "iv" field (used by aes gcm encryption) */ - if (decode_jose_field(decoded_jose, "$.iv", jose_fields->iv)) - goto end; + + if (gcm) { + /* Look for "tag" field (used by aes gcm encryption) */ + if (decode_jose_field(decoded_jose, "$.tag", &jose_fields->tag, 1)) + goto end; + + /* Look for "iv" field (used by aes gcm encryption) */ + if (decode_jose_field(decoded_jose, "$.iv", &jose_fields->iv, 1)) + goto end; + } + + if (ec) { + /* Look for mandatory "epk" field (used by ecdh encryption) */ + const char *tokptr = NULL; + int toklen = 0; + int token_type = mjson_find(b_orig(decoded_jose), b_data(decoded_jose), "$.epk", + &tokptr, &toklen); + if (token_type != MJSON_TOK_OBJECT) + goto end; + + jose_fields->epk = alloc_trash_chunk(); + if (!jose_fields->epk) + goto end; + + if (!chunk_memcpy(jose_fields->epk, tokptr, toklen)) + goto end; + + if (build_EC_PKEY_from_buf(jose_fields->epk, &jose_fields->pubkey)) + goto end; + + /* Look for optional "apu" field (used by ecdh encryption) */ + if (decode_jose_field(decoded_jose, "$.apu", &jose_fields->apu, 0)) + goto end; + + /* Look for optional "apv" field (used by ecdh encryption) */ + if (decode_jose_field(decoded_jose, "$.apv", &jose_fields->apv, 0)) + goto end; + } retval = 1; @@ -210,6 +274,19 @@ end: return retval; } +static void clear_jose_fields(struct jose_fields *jose_fields) +{ + if (!jose_fields) + return; + + free_trash_chunk(jose_fields->tag); + free_trash_chunk(jose_fields->iv); + free_trash_chunk(jose_fields->epk); + EVP_PKEY_free(jose_fields->pubkey); + free_trash_chunk(jose_fields->apu); + free_trash_chunk(jose_fields->apv); +} + /* * Decrypt Encrypted Key encrypted with AES GCM Key Wrap algorithm and @@ -519,8 +596,6 @@ static int sample_conv_jwt_decrypt_secret(const struct arg *args, struct sample struct buffer **cek = NULL; struct buffer *decrypted_cek = NULL; struct buffer *out = NULL; - struct buffer *alg_tag = NULL; - struct buffer *alg_iv = NULL; int size = 0; jwe_alg alg = JWE_ALG_UNMANAGED; jwe_enc enc = JWE_ENC_UNMANAGED; @@ -537,16 +612,6 @@ static int sample_conv_jwt_decrypt_secret(const struct arg *args, struct sample if (jwt_tokenize(input, items, item_num)) 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]) @@ -617,7 +682,8 @@ static int sample_conv_jwt_decrypt_secret(const struct arg *args, struct sample (*cek)->data = cek_size; if (gcm) { - if (!decrypt_cek_aesgcmkw(*cek, alg_tag, alg_iv, decrypted_cek, &secret_smp.data.u.str, alg)) + if (!decrypt_cek_aesgcmkw(*cek, fields.tag, fields.iv, decrypted_cek, + &secret_smp.data.u.str, alg)) goto end; } else { if (!decrypt_cek_aeskw(*cek, decrypted_cek, &secret_smp.data.u.str, alg)) @@ -645,11 +711,10 @@ static int sample_conv_jwt_decrypt_secret(const struct arg *args, struct sample retval = 1; end: + clear_jose_fields(&fields); 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; } @@ -868,6 +933,7 @@ end: free_trash_chunk(decrypted_cek); free_trash_chunk(out); clear_decoded_items(decoded_items); + clear_jose_fields(&fields); return retval; } @@ -1365,6 +1431,7 @@ static void clear_jwk(struct jwk *jwk) jwk->secret = NULL; break; case JWK_KTY_RSA: + case JWK_KTY_EC: EVP_PKEY_free(jwk->pkey); jwk->pkey = NULL; break; @@ -1497,9 +1564,6 @@ static int sample_conv_jwt_decrypt_jwk(const struct arg *args, struct sample *sm 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 = {}; @@ -1524,16 +1588,6 @@ static int sample_conv_jwt_decrypt_jwk(const struct arg *args, struct sample *sm if (jwt_tokenize(input, items, item_num)) 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]) @@ -1620,7 +1674,7 @@ static int sample_conv_jwt_decrypt_jwk(const struct arg *args, struct sample *sm goto end; } else { if (gcm) { - if (!decrypt_cek_aesgcmkw(*cek, alg_tag, alg_iv, decrypted_cek, jwk.secret, alg)) + if (!decrypt_cek_aesgcmkw(*cek, fields.tag, fields.iv, decrypted_cek, jwk.secret, alg)) goto end; } else { if (!decrypt_cek_aeskw(*cek, decrypted_cek, jwk.secret, alg)) @@ -1641,12 +1695,11 @@ static int sample_conv_jwt_decrypt_jwk(const struct arg *args, struct sample *sm end: clear_jwk(&jwk); + clear_jose_fields(&fields); 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; }