MINOR: jwt: Add ecdh-es+axxxkw support in jwt_decrypt_jwk converter

This builds on the ECDH-ES processing and simply requires an extra AES
Key Wrap operation between the built key and the token's CEK.
This commit is contained in:
Remi Tricot-Le Breton 2026-03-10 14:43:45 +01:00 committed by William Lallemand
parent 32d9af559f
commit 3925bb8efc
2 changed files with 96 additions and 26 deletions

View File

@ -95,7 +95,7 @@ haproxy h1 -conf {
http-request set-var(txn.decrypted) var(txn.jwe),jwt_decrypt_jwk(txn.jwk)
.if ssllib_name_startswith(AWS-LC)
acl aws_unmanaged var(txn.jwe),jwt_header_query('$.alg') -m str "A128KW"
acl aws_unmanaged var(txn.jwe),jwt_header_query('$.alg') -m end "A128KW" -m end "A192KW"
http-request set-var(txn.decrypted) str("AWS-LC UNMANAGED") if aws_unmanaged
.endif
@ -277,3 +277,28 @@ client c9 -connect ${h1_mainfe_sock} {
expect resp.http.x-decrypted == "Random test message for ECDH-ES encrypted tokens"
} -run
# ECDH-ES+A___KW
client c10 -connect ${h1_mainfe_sock} {
# ECDH-ES+A128KW
txreq -url "/jwk" -hdr "Authorization: Bearer eyJhbGciOiJFQ0RILUVTK0ExMjhLVyIsImVuYyI6IkExMjhDQkMtSFMyNTYiLCJlcGsiOnsiY3J2IjoiUC0yNTYiLCJrdHkiOiJFQyIsIngiOiJtc2poQktWNW5oNnBjdjhoRnR0UDlFVXRzaURzWG83T3RCekVZYkVJM1EwIiwieSI6IloxQ3FPQlEya1RNR1lENWdMUWJCaHB0MzRKRkR3dW5TX2ZzSmhsMlc1OWcifX0.5l7YaATvAWFJnWK_HsBPmawJ0RMqrkiwyZ9xAuiYCFSiqWWSr8D82A.0sa1s5V2RcDf0FW6hA1lig.z2DVLxtHeY1fPp6dJHiHEuHLVIQHQ10GfYXeFxwNE7JGyto-D3K1elHQn0Yq4Pitaheja21gnXkJajXhOA0rwQ.YmpToFWmj8XQrXMeXTa9eQ" \
-hdr "X-JWK: {\"crv\":\"P-256\",\"d\":\"6qbbYYII1zqqmlDHhTwJt-JYBe-ELI02yAecAx-nD4w\",\"kty\":\"EC\",\"x\":\"bASil7YpthReLltIsaJCaRrE7XtLCRVtOpGtdPO0jH0\",\"y\":\"9xj9qfSrVKFqN3lnaNDXAclGGnfmU_j7xsEocZdYmPs\"}"
rxresp
expect resp.http.x-decrypted ~ "(Random test message for ECDH-ES encrypted tokens|AWS-LC UNMANAGED)"
# ECDH-ES+A192KW
txreq -url "/jwk" -hdr "Authorization: Bearer eyJhbGciOiJFQ0RILUVTK0ExOTJLVyIsImVuYyI6IkExOTJDQkMtSFMzODQiLCJlcGsiOnsiY3J2IjoiUC0zODQiLCJrdHkiOiJFQyIsIngiOiJDcTd3Y0MzUm92VFRZSTMzLU9DcXBocjFlN1NzeEZWY0dOQXhEOEpWZHBRQmROaGg3Z2dLNTJKVkJ1RF9uZXVHIiwieSI6IjlaLU1MV09TQ3VZd0JZVTEtcTd2YUREWUZ1WFhqc1EwSmxpWllLVmdOU0dqVHVLY3VXQnJHemV2RzZEeGgyRHQifX0.75lt6Ixq6UhlN8uiaEphy8SiqEVsuD4Rc3QbFcmP7MJUTyt15LcZ3y-M7TJeNBh3Ajy_6K2WooU.cO9tUaQ2eVo0tIuOqb5_Bw.HQ6DqnLhW2Ad0c78WFGgwCStefYdL37xmh2Fa2mCsVNW5q0K3-xeDHYuIP9Q5xBYEY70U6wV5a0iVN87ii_iMA.feLteQh1ickYVJ2ZZ2whoVzNGRHgUpjp" \
-hdr "X-JWK: {\"alg\":\"ECDH-ES+A192KW\",\"crv\":\"P-384\",\"d\":\"pj6xIezfwtUakkkLtbRQ9FmN6uN1YJ-TSBkWn4awuDfWiHgqpQHA7_L95Hjks1cK\",\"key_ops\":[\"wrapKey\",\"unwrapKey\"],\"kty\":\"EC\",\"x\":\"JO3ojbUYOzoSb-7lAy-c7VhDIjhEtg4zrPn_NJKuGhat-cuI1c4LvOj3n8p3j4bn\",\"y\":\"CA3i4pN7t6liWxQXyxdDp9t79B8uWuubGADJuGn_2_yl6pufhnQ30OBA590fOtEm\"}"
rxresp
expect resp.http.x-decrypted ~ "(Random test message for ECDH-ES\\+A192KW encrypted tokens|AWS-LC UNMANAGED)"
# ECDH-ES+A256KW
txreq -url "/jwk" -hdr "Authorization: Bearer eyJhbGciOiJFQ0RILUVTK0EyNTZLVyIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJlcGsiOnsiY3J2IjoiUC01MjEiLCJrdHkiOiJFQyIsIngiOiJBTFZuZXN6Tl93WVJSWVYtblp3dy1sSkVDTXB2eE1iSENXX3BjY3EyWlF2eFdsNzVKdm5TM3lKbjgzcTE1MlpnWU4zTTB4SUhzQmw1empWZS02OGR4TThwIiwieSI6IkFUX2pGel94RGt0VFY4WWYzZlo1MnRvbE5QWkwwNXlwa0dVTThPWFRNZTBaaVNfYnIzaS0xNHFlWG1OcjA3TFFjNUZMX1VTQkE5WmlyWGRaZkVLUnFqNmEifX0.MqGFvMzpIlwQHeXgPucBkXmS2BaXr2ByUugzD31XrPtxwlWw96vOmfcjSHvda2FGJ1u6InaMMVZMMp75P6AF0kvk8vuM7QF2.kHYblcqwHgXv0xRQrLHwoA.gwFUyTx3RRHWvmqyUL5N6W8HcwbNc1hPTImQPoCNPv6rkhzV1obikVj7sNuTh3Po0nBu2QCKrt-GjJTlD4Q5kw.Q_YZWSkVVxv1rcpySgENN3ZPp-chIYoCGC070kkqiXc" \
-hdr "X-JWK: {\"alg\":\"ECDH-ES+A256KW\",\"crv\":\"P-521\",\"d\":\"AGGLpIzSL1jE34wGa-owWCVt2rgk8j3jqh33QQFKwYCJ9abp3vROyQ-dNv6j6PjrnF1EFyY9dDzChNpWmzoOZAp3\",\"key_ops\":[\"wrapKey\",\"unwrapKey\"],\"kty\":\"EC\",\"x\":\"AD0EIUE6Bt_TDcyOPM6VchRocp7AFSeVd6XkVALWf8AFebeMgKIvJsCsGeRdPTO3vWWrR5AOvvpiBfurb9M9Tus-\",\"y\":\"AOeI5d0iF463g3DolhmVFn6MWk764ONuXRexLApjN-Q6_RkcnCieRSZzqqSPMYuEn-N3i4aYfiEPZV0jk8oZKQMQ\"}"
rxresp
expect resp.http.x-decrypted == "Random test message for ECDH-ES+A256KW encrypted tokens"
} -run

View File

@ -39,9 +39,9 @@ typedef enum {
JWE_ALG_A256KW,
JWE_ALG_DIR,
JWE_ALG_ECDH_ES,
// JWE_ALG_ECDH_ES_A128KW,
// JWE_ALG_ECDH_ES_A192KW,
// JWE_ALG_ECDH_ES_A256KW,
JWE_ALG_ECDH_ES_A128KW,
JWE_ALG_ECDH_ES_A192KW,
JWE_ALG_ECDH_ES_A256KW,
JWE_ALG_A128GCMKW,
JWE_ALG_A192GCMKW,
JWE_ALG_A256GCMKW,
@ -59,9 +59,9 @@ struct alg_enc jwe_algs[] = {
{ "A256KW", JWE_ALG_A256KW },
{ "dir", JWE_ALG_DIR },
{ "ECDH-ES", JWE_ALG_ECDH_ES },
{ "ECDH-ES+A128KW", JWE_ALG_UNMANAGED },
{ "ECDH-ES+A192KW", JWE_ALG_UNMANAGED },
{ "ECDH-ES+A256KW", JWE_ALG_UNMANAGED },
{ "ECDH-ES+A128KW", JWE_ALG_ECDH_ES_A128KW },
{ "ECDH-ES+A192KW", JWE_ALG_ECDH_ES_A192KW },
{ "ECDH-ES+A256KW", JWE_ALG_ECDH_ES_A256KW },
{ "A128GCMKW", JWE_ALG_A128GCMKW },
{ "A192GCMKW", JWE_ALG_A192GCMKW },
{ "A256GCMKW", JWE_ALG_A256GCMKW },
@ -237,6 +237,9 @@ static int parse_jose(struct buffer *decoded_jose, int *alg, int *enc, struct jo
switch (*alg) {
case JWE_ALG_ECDH_ES:
case JWE_ALG_ECDH_ES_A128KW:
case JWE_ALG_ECDH_ES_A192KW:
case JWE_ALG_ECDH_ES_A256KW:
ec = 1;
break;
case JWE_ALG_A128GCMKW:
@ -944,15 +947,25 @@ static int do_decrypt_cek_ec(struct buffer *cek, struct buffer *decrypted_cek, E
int key_size = 0;
struct buffer *derived_secret = NULL;
struct buffer *otherinfo = NULL;
struct buffer *tmpbuf = NULL;
const char *alg_id = NULL;
jwe_alg kw_alg = JWE_ALG_UNMANAGED;
int ecdhes = 0;
unsigned char *concatkdf_ptr = NULL;
size_t *concatkdf_len = 0;
/* rfc7518#section-4.6.2
* Key derivation is performed using the Concat KDF, as defined in
* Section 5.8.1 of [NIST.800-56A], where the Digest Method is SHA-256. */
const EVP_MD *md = EVP_sha256();
int hashlen = EVP_MD_size(md);
EVP_MD_CTX *ctx = NULL;
int keydatalen = 0;
int counter = 0;
int offset = 0;
int reps = 0;
switch(crypt_alg) {
case JWE_ALG_ECDH_ES:
@ -981,6 +994,18 @@ static int do_decrypt_cek_ec(struct buffer *cek, struct buffer *decrypted_cek, E
if (!alg_id)
goto end;
break;
case JWE_ALG_ECDH_ES_A128KW:
key_size = 128;
kw_alg = JWE_ALG_A128KW;
break;
case JWE_ALG_ECDH_ES_A192KW:
key_size = 192;
kw_alg = JWE_ALG_A192KW;
break;
case JWE_ALG_ECDH_ES_A256KW:
key_size = 256;
kw_alg = JWE_ALG_A256KW;
break;
default:
goto end;
}
@ -1011,34 +1036,50 @@ static int do_decrypt_cek_ec(struct buffer *cek, struct buffer *decrypted_cek, E
/* Data derivation as in Section 5.8.1 of [NIST.800-56A]
* https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar2.pdf
*
* For ECDH-ES the buffer built after the concatKDF operation will be
* used directly to decrypt the contents. When ECDH-ES+AES Key Wrap is
* used we must wrap the cek with the built buffer using the right AES
* KW algorithm.
*/
if (ecdhes) {
/* The decrypted cek to be used for actual data decrypt
* operation will be built in the following block. */
int hashlen = EVP_MD_size(md);
if (!ecdhes) {
tmpbuf = alloc_trash_chunk();
if (!tmpbuf)
goto end;
concatkdf_ptr = (unsigned char*)tmpbuf->area;
concatkdf_len = &tmpbuf->data;
} else {
concatkdf_ptr = (unsigned char*)decrypted_cek->area;
concatkdf_len = &decrypted_cek->data;
}
int keydatalen = (key_size >> 3);
/* The decrypted cek to be used for actual data decrypt
* operation will be built in the following block. */
keydatalen = (key_size >> 3);
reps = keydatalen / hashlen;
int reps = keydatalen / hashlen;
int counter = 0;
int offset = 0;
for (counter = 0; counter <= reps; ++counter) {
for (counter = 0; counter <= reps; ++counter) {
uint32_t be_counter = htonl(counter+1);
uint32_t be_counter = htonl(counter+1);
if (EVP_DigestInit_ex(ctx, md, NULL) != 1 ||
EVP_DigestUpdate(ctx, (char*)&be_counter, sizeof(be_counter)) != 1 ||
EVP_DigestUpdate(ctx, b_orig(derived_secret), b_data(derived_secret)) != 1 ||
EVP_DigestUpdate(ctx, b_orig(otherinfo), b_data(otherinfo)) != 1 ||
EVP_DigestFinal_ex(ctx, concatkdf_ptr + offset, NULL) != 1)
goto end;
if (EVP_DigestInit_ex(ctx, md, NULL) != 1 ||
EVP_DigestUpdate(ctx, (char*)&be_counter, sizeof(be_counter)) != 1 ||
EVP_DigestUpdate(ctx, b_orig(derived_secret), b_data(derived_secret)) != 1 ||
EVP_DigestUpdate(ctx, b_orig(otherinfo), b_data(otherinfo)) != 1 ||
EVP_DigestFinal_ex(ctx, (unsigned char*)(decrypted_cek->area + offset), NULL) != 1)
goto end;
offset += hashlen;
offset += hashlen;
}
}
*concatkdf_len = keydatalen;
decrypted_cek->data = keydatalen;
if (!ecdhes) {
/* Need to used the previously generated key to wrap the CEK
* with the "A128KW", "A192KW", or "A256KW" algorithms. */
if (!decrypt_cek_aeskw(cek, decrypted_cek, tmpbuf, kw_alg))
goto end;
}
retval = 0;
@ -1046,6 +1087,7 @@ static int do_decrypt_cek_ec(struct buffer *cek, struct buffer *decrypted_cek, E
end:
free_trash_chunk(derived_secret);
free_trash_chunk(otherinfo);
free_trash_chunk(tmpbuf);
EVP_MD_CTX_free(ctx);
return retval;
}
@ -1860,6 +1902,9 @@ static int sample_conv_jwt_decrypt_jwk(const struct arg *args, struct sample *sm
oct = 1;
break;
case JWE_ALG_ECDH_ES:
case JWE_ALG_ECDH_ES_A128KW:
case JWE_ALG_ECDH_ES_A192KW:
case JWE_ALG_ECDH_ES_A256KW:
ec = 1;
break;
default: